From d625b6167de1a28ea25edea6fbdbe2f486a111af Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Thu, 14 Mar 2024 12:29:38 +0000 Subject: [PATCH 01/98] WIP prototype of MSC4108 --- package.json | 3 +- res/css/structures/auth/_Login.pcss | 19 + src/Lifecycle.ts | 52 ++- src/Login.ts | 3 +- src/MatrixClientPeg.ts | 19 +- src/components/structures/auth/Login.tsx | 66 +++- src/components/views/auth/LoginWithQR.tsx | 333 ++++++++++++++---- src/components/views/auth/LoginWithQRFlow.tsx | 169 +++++++-- .../settings/devices/LoginWithQRSection.tsx | 29 +- .../settings/tabs/user/SessionManagerTab.tsx | 7 +- yarn.lock | 48 +++ 11 files changed, 606 insertions(+), 142 deletions(-) diff --git a/package.json b/package.json index 1c21fff0e93..4409ddf9ddc 100644 --- a/package.json +++ b/package.json @@ -112,7 +112,7 @@ "matrix-widget-api": "^1.5.0", "memoize-one": "^6.0.0", "minimist": "^1.2.5", - "oidc-client-ts": "^3.0.1", + "oidc-client-ts": "github:hughns/oidc-client-ts#hughns/device-flow", "opus-recorder": "^8.0.3", "pako": "^2.0.3", "png-chunks-extract": "^1.0.0", @@ -125,6 +125,7 @@ "react-blurhash": "^0.3.0", "react-dom": "17.0.2", "react-focus-lock": "^2.5.1", + "react-qr-reader": "^3.0.0-beta-1", "react-transition-group": "^4.4.1", "rfc4648": "^1.4.0", "sanitize-filename": "^1.6.3", diff --git a/res/css/structures/auth/_Login.pcss b/res/css/structures/auth/_Login.pcss index aa4244bcfbd..fe5e4bc9ad0 100644 --- a/res/css/structures/auth/_Login.pcss +++ b/res/css/structures/auth/_Login.pcss @@ -104,3 +104,22 @@ div.mx_AccessibleButton_kind_link.mx_Login_forgot { width: 100%; margin-bottom: 16px; } + +.mx_Login_withQR_or { + font-size: 14px; + text-align: center; + margin-top: -14px; + color: #737D8C; + margin-bottom: 10px; +} + +.mx_Login_withQR { + display: block !important; + margin-bottom: 20px; + svg { + height: 1em; + vertical-align: middle; + margin-right: 10px; + padding-bottom: 2px; + } +} diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts index b6fce3b6365..8225ddb4ae2 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -18,11 +18,13 @@ limitations under the License. */ import { ReactNode } from "react"; -import { createClient, MatrixClient, SSOAction, OidcTokenRefresher } from "matrix-js-sdk/src/matrix"; +import { createClient, MatrixClient, SSOAction, OidcTokenRefresher, validateIdToken } from "matrix-js-sdk/src/matrix"; import { InvalidStoreError } from "matrix-js-sdk/src/errors"; import { IEncryptedPayload } from "matrix-js-sdk/src/crypto/aes"; import { QueryDict } from "matrix-js-sdk/src/utils"; import { logger } from "matrix-js-sdk/src/logger"; +import { DeviceAccessTokenResponse, IdTokenClaims, OidcClient } from "oidc-client-ts"; +import { QRSecretsBundle } from "matrix-js-sdk/src/crypto-api"; import { IMatrixClientCreds, MatrixClientPeg } from "./MatrixClientPeg"; import SecurityCustomisations from "./customisations/Security"; @@ -284,6 +286,41 @@ export async function attemptDelegatedAuthLogin( return attemptTokenLogin(queryParams, defaultDeviceDisplayName, fragmentAfterLogin); } +export async function completeDeviceAuthorizationGrant( + oidcClient: OidcClient, + { access_token: accessToken, refresh_token: refreshToken, id_token: idToken }: DeviceAccessTokenResponse, + homeserverUrl: string, + identityServerUrl?: string, +): Promise<{ credentials?: IMatrixClientCreds }> { + try { + const { + user_id: userId, + device_id: deviceId, + is_guest: isGuest, + } = await getUserIdFromAccessToken(accessToken, homeserverUrl, identityServerUrl); + + const credentials = { + accessToken, + refreshToken, + homeserverUrl, + identityServerUrl, + deviceId, + userId, + isGuest, + }; + + logger.info("Logged in via OIDC Device Authorization Grant"); + await onSuccessfulDelegatedAuthLogin(credentials); + const idTokenClaims = validateIdToken(idToken, oidcClient.settings.authority, oidcClient.settings.client_id, undefined) as IdTokenClaims; + persistOidcAuthenticatedSettings(oidcClient.settings.client_id, oidcClient.settings.authority, idTokenClaims); + return { credentials }; + } catch (error) { + logger.error("Failed to login via OIDC Device Authorization Grant", error); + + await onFailedDelegatedAuthLogin(getOidcErrorMessage(error as Error)); + return {}; + }} + /** * Attempt to login by completing OIDC authorization code flow * @param queryParams string->string map of the query-parameters extracted from the real query-string of the starting URI. @@ -700,6 +737,9 @@ export async function setLoggedIn(credentials: IMatrixClientCreds): Promise { + return doSetLoggedIn(credentials, false, secrets); +} /** * Hydrates an existing session by using the credentials provided. This will * not clear any local storage, unlike setLoggedIn(). @@ -785,7 +825,7 @@ async function createOidcTokenRefresher(credentials: IMatrixClientCreds): Promis * * @returns {Promise} promise which resolves to the new MatrixClient once it has been started */ -async function doSetLoggedIn(credentials: IMatrixClientCreds, clearStorageEnabled: boolean): Promise { +async function doSetLoggedIn(credentials: IMatrixClientCreds, clearStorageEnabled: boolean, secrets?: QRSecretsBundle): Promise { checkSessionLock(); credentials.guest = Boolean(credentials.guest); @@ -856,7 +896,7 @@ async function doSetLoggedIn(credentials: IMatrixClientCreds, clearStorageEnable checkSessionLock(); dis.fire(Action.OnLoggedIn); - await startMatrixClient(client, /*startSyncing=*/ !softLogout); + await startMatrixClient(client, /*startSyncing=*/ !softLogout, secrets); return client; } @@ -994,7 +1034,7 @@ export function isLoggingOut(): boolean { * @param {boolean} startSyncing True (default) to actually start * syncing the client. */ -async function startMatrixClient(client: MatrixClient, startSyncing = true): Promise { +async function startMatrixClient(client: MatrixClient, startSyncing = true, secrets?: QRSecretsBundle): Promise { logger.log(`Lifecycle: Starting MatrixClient`); // dispatch this before starting the matrix client: it's used @@ -1025,10 +1065,10 @@ async function startMatrixClient(client: MatrixClient, startSyncing = true): Pro // index (e.g. the FilePanel), therefore initialize the event index // before the client. await EventIndexPeg.init(); - await MatrixClientPeg.start(); + await MatrixClientPeg.start(secrets); } else { logger.warn("Caller requested only auxiliary services be started"); - await MatrixClientPeg.assign(); + await MatrixClientPeg.assign(secrets); } checkSessionLock(); diff --git a/src/Login.ts b/src/Login.ts index 4f198fc634e..3d99548d942 100644 --- a/src/Login.ts +++ b/src/Login.ts @@ -123,7 +123,7 @@ export default class Login { SdkConfig.get().oidc_static_clients, isRegistration, ); - return [oidcFlow]; + return [oidcFlow, { type: "loginWithQR" }]; // PROTOTYPE: this should probably be behind a feature flag } catch (error) { logger.error(error); } @@ -138,6 +138,7 @@ export default class Login { (f) => f.type === "m.login.sso" && DELEGATED_OIDC_COMPATIBILITY.findIn(f), ); this.flows = oidcCompatibilityFlow ? [oidcCompatibilityFlow] : flows; + this.flows.push({ type: "loginWithQR" }); // PROTOTYPE: this should probably be behind a feature flag return this.flows; } diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index 47585173904..a0272a6cc0f 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -54,6 +54,7 @@ import { formatList } from "./utils/FormattingUtils"; import SdkConfig from "./SdkConfig"; import { Features } from "./settings/Settings"; import { PhasedRolloutFeature } from "./utils/PhasedRolloutFeature"; +import { QRSecretsBundle } from "matrix-js-sdk/src/crypto-api"; export interface IMatrixClientCreds { homeserverUrl: string; @@ -88,8 +89,8 @@ export interface IMatrixClientPeg { get(): MatrixClient | null; safeGet(): MatrixClient; unset(): void; - assign(): Promise; - start(): Promise; + assign(secrets?: QRSecretsBundle): Promise; + start(secrets?: QRSecretsBundle): Promise; /** * If we've registered a user ID we set this to the ID of the @@ -236,7 +237,7 @@ class MatrixClientPegClass implements IMatrixClientPeg { PlatformPeg.get()?.reload(); }; - public async assign(): Promise { + public async assign(secrets?: QRSecretsBundle): Promise { if (!this.matrixClient) { throw new Error("createClient must be called first"); } @@ -263,7 +264,7 @@ class MatrixClientPegClass implements IMatrixClientPeg { // try to initialise e2e on the new client if (!SettingsStore.getValue("lowBandwidth")) { - await this.initClientCrypto(); + await this.initClientCrypto(secrets); } const opts = utils.deepCopy(this.opts); @@ -298,7 +299,7 @@ class MatrixClientPegClass implements IMatrixClientPeg { /** * Attempt to initialize the crypto layer on a newly-created MatrixClient */ - private async initClientCrypto(): Promise { + private async initClientCrypto(secrets?: QRSecretsBundle): Promise { if (!this.matrixClient) { throw new Error("createClient must be called first"); } @@ -336,6 +337,10 @@ class MatrixClientPegClass implements IMatrixClientPeg { if (useRustCrypto) { await this.matrixClient.initRustCrypto(); + if (secrets) { + this.matrixClient.getCrypto()?.importSecretsForQRLogin(secrets); + } + StorageManager.setCryptoInitialised(true); // TODO: device dehydration and whathaveyou return; @@ -363,8 +368,8 @@ class MatrixClientPegClass implements IMatrixClientPeg { } } - public async start(): Promise { - const opts = await this.assign(); + public async start(secrets?: QRSecretsBundle): Promise { + const opts = await this.assign(secrets); logger.log(`MatrixClientPeg: really starting MatrixClient`); await this.matrixClient!.startClient(opts); diff --git a/src/components/structures/auth/Login.tsx b/src/components/structures/auth/Login.tsx index 5fadde7cbea..c74643a0c08 100644 --- a/src/components/structures/auth/Login.tsx +++ b/src/components/structures/auth/Login.tsx @@ -40,6 +40,8 @@ import { ValidatedServerConfig } from "../../../utils/ValidatedServerConfig"; import { filterBoolean } from "../../../utils/arrays"; import { Features } from "../../../settings/Settings"; import { startOidcLogin } from "../../../utils/oidc/authorize"; +import LoginWithQR, { Mode } from "../../views/auth/LoginWithQR"; +import { Icon as QRIcon } from "../../../../res/img/element-icons/qrcode.svg"; interface IProps { serverConfig: ValidatedServerConfig; @@ -89,6 +91,7 @@ interface IState { serverIsAlive: boolean; serverErrorIsFatal: boolean; serverDeadError?: ReactNode; + loginWithQrInProgress: boolean; } type OnPasswordLogin = { @@ -125,6 +128,7 @@ export default class LoginComponent extends React.PureComponent serverIsAlive: true, serverErrorIsFatal: false, serverDeadError: "", + loginWithQrInProgress: false, }; // map from login step type to a function which will render a control @@ -138,6 +142,7 @@ export default class LoginComponent extends React.PureComponent // eslint-disable-next-line @typescript-eslint/naming-convention "m.login.sso": () => this.renderSsoStep("sso"), "oidcNativeFlow": () => this.renderOidcNativeStep(), + "loginWithQR": () => this.renderLoginWithQRStep(), }; } @@ -421,7 +426,7 @@ export default class LoginComponent extends React.PureComponent if (!this.state.flows) return null; // this is the ideal order we want to show the flows in - const order = ["oidcNativeFlow", "m.login.password", "m.login.sso"]; + const order = ["loginWithQR", "oidcNativeFlow", "m.login.password", "m.login.sso"]; const flows = filterBoolean(order.map((type) => this.state.flows?.find((flow) => flow.type === type))); return ( @@ -489,6 +494,31 @@ export default class LoginComponent extends React.PureComponent ); }; + private startLoginWithQR = (): void => { + this.setState({ loginWithQrInProgress: true }); + }; + + private renderLoginWithQRStep = (): JSX.Element | null => { + return ( + <> +

or

+ + + Sign in with QR code + + + ); + }; + + private onLoginWithQRFinished = (data: IMatrixClientCreds): void => { + if (!data) { + this.setState({ loginWithQrInProgress: false }); + } else { + // PROTOTYPE: I'm not sure if this is the right way to do this. Obviously we don't have a password + this.props.onLoggedIn(data, ""); + } + }; + public render(): React.ReactNode { const loader = this.isBusy() && !this.state.busyLoggingIn ? ( @@ -548,20 +578,26 @@ export default class LoginComponent extends React.PureComponent return ( - -

- {_t("action|sign_in")} - {loader} -

- {errorTextSection} - {serverDeadSection} - - {this.renderLoginComponentForFlows()} - {footer} -
+ {this.state.loginWithQrInProgress ? ( + + + + ) : ( + +

+ {_t("action|sign_in")} + {loader} +

+ {errorTextSection} + {serverDeadSection} + + {this.renderLoginComponentForFlows()} + {footer} +
+ )}
); } diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index 1e2efb5106f..e23c1b20f31 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -15,15 +15,23 @@ limitations under the License. */ import React from "react"; -import { MSC3906Rendezvous, MSC3906RendezvousPayload, RendezvousFailureReason } from "matrix-js-sdk/src/rendezvous"; -import { MSC3886SimpleHttpRendezvousTransport } from "matrix-js-sdk/src/rendezvous/transports"; -import { MSC3903ECDHPayload, MSC3903ECDHv2RendezvousChannel } from "matrix-js-sdk/src/rendezvous/channels"; +import { + MSC4108SignInWithQR, + RendezvousFailureReason, + RendezvousIntent, + buildLoginFromScannedCode, +} from "matrix-js-sdk/src/rendezvous"; +import { MSC4108RendezvousSession } from "matrix-js-sdk/src/rendezvous/transports"; +import { MSC4108SecureChannel } from "matrix-js-sdk/src/rendezvous/channels"; import { logger } from "matrix-js-sdk/src/logger"; -import { HTTPError, MatrixClient } from "matrix-js-sdk/src/matrix"; +import { MatrixClient, discoverAndValidateOIDCIssuerWellKnown, generateScope } from "matrix-js-sdk/src/matrix"; +import { OnResultFunction } from "react-qr-reader"; +import { OidcClient } from "oidc-client-ts"; -import { _t } from "../../../languageHandler"; -import { wrapRequestWithDialog } from "../../../utils/UserInteractiveAuth"; import LoginWithQRFlow from "./LoginWithQRFlow"; +import { getOidcClientId } from "../../../utils/oidc/registerClient"; +import SdkConfig from "../../../SdkConfig"; +import { completeDeviceAuthorizationGrant, completeLoginWithQr } from "../../../Lifecycle"; /** * The intention of this enum is to have a mode that scans a QR code instead of generating one. @@ -33,16 +41,20 @@ export enum Mode { * A QR code with be generated and shown */ Show = "show", + Scan = "scan", } export enum Phase { - Loading, - ShowingQR, - Connecting, - Connected, - WaitingForDevice, - Verifying, - Error, + Loading = "loading", + ScanningQR = "scanningQR", + ShowingQR = "showingQR", + Connecting = "connecting", + OutOfBandConfirmation = "outOfBandConfirmation", + ShowChannelSecure = "showChannelSecure", + WaitingForDevice = "waitingForDevice", + Verifying = "verifying", + Continue = "continue", + Error = "error", } export enum Click { @@ -51,20 +63,26 @@ export enum Click { Approve, TryAgain, Back, + ScanQr, + ShowQr, } interface IProps { - client: MatrixClient; + client?: MatrixClient; mode: Mode; onFinished(...args: any): void; } interface IState { phase: Phase; - rendezvous?: MSC3906Rendezvous; - confirmationDigits?: string; - failureReason?: FailureReason; + rendezvous?: MSC4108SignInWithQR; + verificationUri?: string; + userCode?: string; + failureReason?: RendezvousFailureReason; mediaPermissionError?: boolean; + lastScannedCode?: Buffer; + ourIntent: RendezvousIntent; + homeserverBaseUrl?: string; } export enum LoginWithQRFailureReason { @@ -86,6 +104,9 @@ export default class LoginWithQR extends React.Component { this.state = { phase: Phase.Loading, + ourIntent: this.props.client + ? RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE + : RendezvousIntent.LOGIN_ON_NEW_DEVICE, }; } @@ -100,15 +121,19 @@ export default class LoginWithQR extends React.Component { } private async updateMode(mode: Mode): Promise { + logger.info(`updateMode: ${mode}`); this.setState({ phase: Phase.Loading }); if (this.state.rendezvous) { const rendezvous = this.state.rendezvous; rendezvous.onFailure = undefined; - await rendezvous.cancel(RendezvousFailureReason.UserCancelled); + // await rendezvous.cancel(RendezvousFailureReason.UserCancelled); this.setState({ rendezvous: undefined }); } if (mode === Mode.Show) { - await this.generateCode(); + await this.generateAndShowCode(); + } else { + await this.requestMediaPermissions(); + this.setState({ phase: Phase.ScanningQR }); } } @@ -121,69 +146,44 @@ export default class LoginWithQR extends React.Component { } } - private approveLogin = async (): Promise => { - if (!this.state.rendezvous) { - throw new Error("Rendezvous not found"); - } - this.setState({ phase: Phase.Loading }); - - try { - logger.info("Requesting login token"); - - const { login_token: loginToken } = await wrapRequestWithDialog(this.props.client.requestLoginToken, { - matrixClient: this.props.client, - title: _t("auth|qr_code_login|sign_in_new_device"), - })(); - - this.setState({ phase: Phase.WaitingForDevice }); + private getOidcClient = async (homeserverBaseUrl: string): Promise => { + // oidc discovery + const tempClient = new MatrixClient({ baseUrl: homeserverBaseUrl }); + // this should fall back to the well-known + const { issuer } = await tempClient.getAuthIssuer(); + // AutoDiscovery; + const metadata = await discoverAndValidateOIDCIssuerWellKnown(issuer); + // oidc registration + const clientId = await getOidcClientId(metadata, SdkConfig.get().oidc_static_clients); + + const scope = generateScope(); + const oidcClient = new OidcClient({ + ...metadata, + client_id: clientId, + redirect_uri: window.location.href, + authority: issuer, + scope, + }); - const newDeviceId = await this.state.rendezvous.approveLoginOnExistingDevice(loginToken); - if (!newDeviceId) { - // user denied - return; - } - if (!this.props.client.getCrypto()) { - // no E2EE to set up - this.props.onFinished(true); - return; - } - this.setState({ phase: Phase.Verifying }); - await this.state.rendezvous.verifyNewDeviceOnExistingDevice(); - // clean up our state: - try { - await this.state.rendezvous.close(); - } finally { - this.setState({ rendezvous: undefined }); - } - this.props.onFinished(true); - } catch (e) { - logger.error("Error whilst approving sign in", e); - if (e instanceof HTTPError && e.httpStatus === 429) { - // 429: rate limit - this.setState({ phase: Phase.Error, failureReason: LoginWithQRFailureReason.RateLimited }); - return; - } - this.setState({ phase: Phase.Error, failureReason: RendezvousFailureReason.Unknown }); - } + return oidcClient; }; - private generateCode = async (): Promise => { - let rendezvous: MSC3906Rendezvous; + private generateAndShowCode = async (): Promise => { + let rendezvous: MSC4108SignInWithQR; try { - const fallbackRzServer = this.props.client.getClientWellKnown()?.["io.element.rendezvous"]?.server; - const transport = new MSC3886SimpleHttpRendezvousTransport({ + const fallbackRzServer = + this.props.client?.getClientWellKnown()?.["io.element.rendezvous"]?.server ?? + "https://rendezvous.lab.element.dev"; + const transport = new MSC4108RendezvousSession({ onFailure: this.onFailure, client: this.props.client, fallbackRzServer, }); + await transport.send(""); - const channel = new MSC3903ECDHv2RendezvousChannel( - transport, - undefined, - this.onFailure, - ); + const channel = new MSC4108SecureChannel(transport, undefined, this.onFailure); - rendezvous = new MSC3906Rendezvous(channel, this.props.client, this.onFailure); + rendezvous = new MSC4108SignInWithQR(channel, false, this.props.client, this.onFailure); await rendezvous.generateCode(); this.setState({ @@ -198,8 +198,27 @@ export default class LoginWithQR extends React.Component { } try { - const confirmationDigits = await rendezvous.startAfterShowingCode(); - this.setState({ phase: Phase.Connected, confirmationDigits }); + const { homeserverBaseUrl } = await rendezvous.loginStep1(); + + if (this.state.ourIntent === RendezvousIntent.LOGIN_ON_NEW_DEVICE) { + if (!homeserverBaseUrl) { + throw new Error("We don't know the homeserver"); + } + // PROTOTYPE: this feels bad, we have taken the homeserver URL that was sent by the other device + // before the channel was confirmed to the secure + // this could cause us to leak the OIDC registration data to a malicious server + // TODO: has this been implemented incorrectly? or is it a flaw in MSC4108? + await rendezvous.loginStep2(await this.getOidcClient(homeserverBaseUrl)); + } + + const { verificationUri } = await rendezvous.loginStep3(); + this.setState({ + phase: Phase.OutOfBandConfirmation, + verificationUri, + homeserverBaseUrl, + }); + + // we ask the user to confirm that the channel is secure } catch (e) { logger.error("Error whilst doing QR login", e); // only set to error phase if it hasn't already been set by onFailure or similar @@ -209,6 +228,139 @@ export default class LoginWithQR extends React.Component { } }; + private processScannedCode = async (scannedCode: Buffer): Promise => { + logger.info(scannedCode.toString()); + try { + if (this.state.lastScannedCode?.equals(scannedCode)) { + return; // suppress duplicate scans + } + if (this.state.rendezvous) { + await this.state.rendezvous.cancel(RendezvousFailureReason.UserCancelled); + this.reset(); + } + + const { signin: rendezvous, homeserverBaseUrl: homeserverBaseUrlFromCode } = + await buildLoginFromScannedCode(this.props.client, scannedCode, this.onFailure); + + this.setState({ + phase: Phase.Connecting, + lastScannedCode: scannedCode, + rendezvous, + failureReason: undefined, + }); + + const { homeserverBaseUrl: homeserverBaseUrlFromStep1 } = await rendezvous.loginStep1(); + + if (this.state.ourIntent === RendezvousIntent.LOGIN_ON_NEW_DEVICE) { + const homeserverBaseUrl = homeserverBaseUrlFromCode; + + if (!homeserverBaseUrl) { + throw new Error("We don't know the homeserver"); + } + const oidcClient = await this.getOidcClient(homeserverBaseUrl); + await rendezvous.loginStep2(oidcClient); + + this.setState({ + phase: Phase.ShowChannelSecure, + }); + const { userCode } = await rendezvous.loginStep3(); + this.setState({ + phase: Phase.ShowChannelSecure, + userCode, + }); + + // wait for grant: + const tokenResponse = await rendezvous.loginStep4(); + + const { credentials } = await completeDeviceAuthorizationGrant( + oidcClient, + tokenResponse, + homeserverBaseUrl, + undefined, + ); + + if (!credentials) { + throw new Error("Failed to complete device authorization grant"); + } + + // wait for secrets: + const { secrets } = await rendezvous.loginStep5(credentials.deviceId); + + await completeLoginWithQr(credentials, secrets); + + // done + this.props.onFinished(credentials); + } else { + const homeserverBaseUrl = homeserverBaseUrlFromStep1; + this.setState({ + phase: Phase.ShowChannelSecure, + }); + const { verificationUri } = await rendezvous.loginStep3(); + this.setState({ + phase: Phase.Continue, + verificationUri, + homeserverBaseUrl, + }); + } + } catch (e) { + alert(e); + throw e; + } + }; + + private approveLoginAfterShowingCode = async (): Promise => { + if (!this.state.rendezvous) { + throw new Error("Rendezvous not found"); + } + + if (this.state.ourIntent === RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE) { + this.setState({ phase: Phase.Loading }); + + if (this.state.verificationUri) { + window.open(this.state.verificationUri, "_blank"); + } + + this.setState({ phase: Phase.WaitingForDevice }); + + // send secrets + await this.state.rendezvous.loginStep5(); + + // done + this.props.onFinished(true); + } else { + if (!this.state.homeserverBaseUrl) { + throw new Error("We don't know the homeserver"); + } + const { homeserverBaseUrl } = this.state; + const oidcClient = await this.getOidcClient(homeserverBaseUrl); + await this.state.rendezvous.loginStep2(oidcClient); + const { userCode } = await this.state.rendezvous.loginStep3(); + this.setState({ phase: Phase.WaitingForDevice, userCode }); + + // wait for grant: + const tokenResponse = await this.state.rendezvous.loginStep4(); + + const { credentials } = await completeDeviceAuthorizationGrant( + oidcClient, + tokenResponse, + homeserverBaseUrl, + undefined, + ); + + if (!credentials) { + throw new Error("Failed to complete device authorization grant"); + } + + // wait for secrets + const { secrets } = await this.state.rendezvous.loginStep5(credentials.deviceId); + + await completeLoginWithQr(credentials, secrets); + + // done + this.props.onFinished(credentials); + } + }; + private onFailure = (reason: RendezvousFailureReason): void => { logger.info(`Rendezvous failed: ${reason}`); this.setState({ phase: Phase.Error, failureReason: reason }); @@ -217,8 +369,12 @@ export default class LoginWithQR extends React.Component { public reset(): void { this.setState({ rendezvous: undefined, - confirmationDigits: undefined, + verificationUri: undefined, failureReason: undefined, + userCode: undefined, + homeserverBaseUrl: undefined, + lastScannedCode: undefined, + mediaPermissionError: false, }); } @@ -230,7 +386,7 @@ export default class LoginWithQR extends React.Component { this.props.onFinished(false); break; case Click.Approve: - await this.approveLogin(); + await this.approveLoginAfterShowingCode(); break; case Click.Decline: await this.state.rendezvous?.declineLoginOnExistingDevice(); @@ -245,17 +401,40 @@ export default class LoginWithQR extends React.Component { await this.state.rendezvous?.cancel(RendezvousFailureReason.UserCancelled); this.props.onFinished(false); break; + case Click.ScanQr: + await this.updateMode(Mode.Scan); + break; + case Click.ShowQr: + await this.updateMode(Mode.Show); + break; + } + }; + + private onScannedQRCode: OnResultFunction = (result, error): void => { + if (result) { + void this.processScannedCode(Buffer.from((result.getResultMetadata().get(2) as [Uint8Array])[0])); + } + }; + + private requestMediaPermissions = async (): Promise => { + try { + await navigator.mediaDevices.getUserMedia({ video: true }); + this.setState({ mediaPermissionError: false }); + } catch (err) { + this.setState({ mediaPermissionError: true }); } }; public render(): React.ReactNode { + logger.info("LoginWithQR render"); return ( ); } diff --git a/src/components/views/auth/LoginWithQRFlow.tsx b/src/components/views/auth/LoginWithQRFlow.tsx index 526496246a9..ce2ad80b672 100644 --- a/src/components/views/auth/LoginWithQRFlow.tsx +++ b/src/components/views/auth/LoginWithQRFlow.tsx @@ -16,6 +16,8 @@ limitations under the License. import React from "react"; import { RendezvousFailureReason } from "matrix-js-sdk/src/rendezvous"; +import { QrReader, OnResultFunction } from "react-qr-reader"; +import { logger } from "matrix-js-sdk/src/logger"; import { _t } from "../../../languageHandler"; import AccessibleButton from "../elements/AccessibleButton"; @@ -24,15 +26,16 @@ import Spinner from "../elements/Spinner"; import { Icon as BackButtonIcon } from "../../../../res/img/element-icons/back.svg"; import { Icon as DevicesIcon } from "../../../../res/img/element-icons/devices.svg"; import { Icon as WarningBadge } from "../../../../res/img/element-icons/warning-badge.svg"; -import { Icon as InfoIcon } from "../../../../res/img/element-icons/i.svg"; -import { Click, FailureReason, LoginWithQRFailureReason, Phase } from "./LoginWithQR"; +import { Icon as CheckmarkIcon } from "../../../../res/img/element-icons/check.svg"; +import { Click, Phase } from "./LoginWithQR"; interface IProps { phase: Phase; - code?: string; + code?: Buffer; onClick(type: Click): Promise; - failureReason?: FailureReason; - confirmationDigits?: string; + failureReason?: RendezvousFailureReason; + onScannedQRCode?: OnResultFunction; + userCode?: string; } /** @@ -69,7 +72,19 @@ export default class LoginWithQRFlow extends React.Component { ); }; + private viewFinder(): JSX.Element { + return ( + + + + + + + ); + } + public render(): React.ReactNode { + logger.info(`LoginWithQRFlow render: phase=${this.props.phase}`); let title = ""; let titleIcon: JSX.Element | undefined; let main: JSX.Element | undefined; @@ -102,9 +117,6 @@ export default class LoginWithQRFlow extends React.Component { case RendezvousFailureReason.UserCancelled: cancellationMessage = _t("auth|qr_code_login|error_request_cancelled"); break; - case LoginWithQRFailureReason.RateLimited: - cancellationMessage = _t("auth|qr_code_login|error_rate_limited"); - break; case RendezvousFailureReason.Unknown: cancellationMessage = _t("auth|qr_code_login|error_unexpected"); break; @@ -133,68 +145,133 @@ export default class LoginWithQRFlow extends React.Component { ); break; - case Phase.Connected: - title = _t("auth|qr_code_login|devices_connected"); + case Phase.OutOfBandConfirmation: + title = "Do you see a green checkmark on your other device?"; titleIcon = ; backButton = false; main = ( <> -

{_t("auth|qr_code_login|confirm_code_match")}

-
{this.props.confirmationDigits}
+

+ Confirm that you see a green checkmark on both devices to to verify that the connection is + secure. +

- +
-
{_t("auth|qr_code_login|approve_access_warning")}
+ {this.props.userCode ? ( +
+

Security code

+

If asked, enter the code below on your other device.

+

{this.props.userCode}

+
+ ) : null} ); buttons = ( <> + + Yes, I do + - {_t("action|cancel")} + No, I don't + + ); + break; + case Phase.Continue: + // PROTOTYPE: I don't think we would offer the ability to scan from a web browser, so this is really just for the prototype. + title = "Go to your account to continue"; + titleIcon = ; + backButton = false; + main = ( + <> +

Open your servername.io account to link your new device.

+

+ This screen is only needed due to Web Browser UX restrictions. If this was a native mobile + app like Element X then the OIDC Provider consent screen could be opened automatically. + Also, we don't plan to offer the ability to scan from a web browser so this is a non-issue. +

+
+
+ +
+
+ + ); + + buttons = ( + <> - {_t("action|approve")} + Continue ); break; + case Phase.ShowChannelSecure: + title = "Go to your other device"; + titleIcon = ; + backButton = false; + main = ( + <> +

You’ll be asked to confirm that you can see a green checkmark on this device..

+
+
+ +
+
+ {this.props.userCode ? ( +
+

Security code

+

If asked, enter the code below on your other device.

+

{this.props.userCode}

+
+ ) : null} + + ); + break; case Phase.ShowingQR: title = _t("settings|sessions|sign_in_with_qr"); if (this.props.code) { const code = (
- +
); main = ( <> -

{_t("auth|qr_code_login|scan_code_instruction")}

+

Do something:

    -
  1. {_t("auth|qr_code_login|start_at_sign_in_screen")}
  2. -
  3. - {_t("auth|qr_code_login|select_qr_code", { - scanQRCode: _t("auth|qr_code_login|scan_qr_code"), - })} -
  4. -
  5. {_t("auth|qr_code_login|review_and_approve")}
  6. +
  7. A
  8. +
  9. B
  10. +
  11. C
{code} ); + buttons = ( + + Scan QR code instead + + ); } else { main = this.simpleSpinner(); buttons = this.cancelButton(); @@ -208,7 +285,18 @@ export default class LoginWithQRFlow extends React.Component { buttons = this.cancelButton(); break; case Phase.WaitingForDevice: - main = this.simpleSpinner(_t("auth|qr_code_login|waiting_for_device")); + main = ( + <> + {this.simpleSpinner(_t("auth|qr_code_login|waiting_for_device"))} + {this.props.userCode ? ( +
+

Security code

+

If asked, enter the code below on your other device.

+

{this.props.userCode}

+
+ ) : null} + + ); buttons = this.cancelButton(); break; case Phase.Verifying: @@ -216,6 +304,29 @@ export default class LoginWithQRFlow extends React.Component { centreTitle = true; main = this.simpleSpinner(_t("auth|qr_code_login|completing_setup")); break; + case Phase.ScanningQR: + title = _t("settings|sessions|sign_in_with_qr"); + main = ( + <> +

Line up the QR code in the square below:

+ + + ); + buttons = ( + + Show QR code instead + + ); + break; } return ( diff --git a/src/components/views/settings/devices/LoginWithQRSection.tsx b/src/components/views/settings/devices/LoginWithQRSection.tsx index 036597cfe9c..74d007ed2d7 100644 --- a/src/components/views/settings/devices/LoginWithQRSection.tsx +++ b/src/components/views/settings/devices/LoginWithQRSection.tsx @@ -21,7 +21,10 @@ import { GET_LOGIN_TOKEN_CAPABILITY, Capabilities, IClientWellKnown, + OidcClientConfig, + AutoDiscoveryState, } from "matrix-js-sdk/src/matrix"; +import { logger } from "matrix-js-sdk/src/logger"; import { _t } from "../../../../languageHandler"; import AccessibleButton from "../../elements/AccessibleButton"; @@ -32,6 +35,7 @@ interface IProps { versions?: IServerVersions; capabilities?: Capabilities; wellKnown?: IClientWellKnown; + authenticationConfig?: (OidcClientConfig & AutoDiscoveryState) | AutoDiscoveryState; } export default class LoginWithQRSection extends React.Component { @@ -40,15 +44,30 @@ export default class LoginWithQRSection extends React.Component { } public render(): JSX.Element | null { - // Needs server support for get_login_token and MSC3886: + // Needs server support for (get_login_token or OIDC Device Authorization Grant) and MSC3886: // in r0 of MSC3882 it is exposed as a feature flag, but in stable and unstable r1 it is a capability - const capability = GET_LOGIN_TOKEN_CAPABILITY.findIn(this.props.capabilities); + const loginTokenCapability = GET_LOGIN_TOKEN_CAPABILITY.findIn( + this.props.capabilities, + ); const getLoginTokenSupported = - !!this.props.versions?.unstable_features?.["org.matrix.msc3882"] || !!capability?.enabled; + !!this.props.versions?.unstable_features?.["org.matrix.msc3882"] || !!loginTokenCapability?.enabled; const msc3886Supported = !!this.props.versions?.unstable_features?.["org.matrix.msc3886"] || - this.props.wellKnown?.["io.element.rendezvous"]?.server; - const offerShowQr = getLoginTokenSupported && msc3886Supported; + !!this.props.wellKnown?.["io.element.rendezvous"]?.server; + + const deviceAuthorizationGrantSupported = + this.props.authenticationConfig && + "metadata" in this.props.authenticationConfig && + this.props.authenticationConfig.metadata.grant_types_supported.includes( + "urn:ietf:params:oauth:grant-type:device_code", + ); + + logger.info( + `getLoginTokenSupported: ${getLoginTokenSupported} msc3886Supported: ${msc3886Supported} deviceAuthorizationGrantSupported: ${deviceAuthorizationGrantSupported}`, + ); + // PROTOTYPE: we hard code this to always show: + // We aren't checking for MSC4108 support + const offerShowQr = true || ((getLoginTokenSupported || deviceAuthorizationGrantSupported) && msc3886Supported); // don't show anything if no method is available if (!offerShowQr) { diff --git a/src/components/views/settings/tabs/user/SessionManagerTab.tsx b/src/components/views/settings/tabs/user/SessionManagerTab.tsx index fc215f069ae..62c7c27ae3f 100644 --- a/src/components/views/settings/tabs/user/SessionManagerTab.tsx +++ b/src/components/views/settings/tabs/user/SessionManagerTab.tsx @@ -15,7 +15,7 @@ limitations under the License. */ import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; -import { MatrixClient } from "matrix-js-sdk/src/matrix"; +import { AutoDiscovery, MatrixClient } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; import { _t } from "../../../../../languageHandler"; @@ -184,6 +184,10 @@ const SessionManagerTab: React.FC = () => { const clientVersions = useAsyncMemo(() => matrixClient.getVersions(), [matrixClient]); const capabilities = useAsyncMemo(async () => matrixClient?.getCapabilities(), [matrixClient]); const wellKnown = useMemo(() => matrixClient?.getClientWellKnown(), [matrixClient]); + const authenticationConfig = useAsyncMemo( + async () => (wellKnown ? AutoDiscovery.discoverAndValidateAuthenticationConfig(wellKnown) : undefined), + [wellKnown], + ); const onDeviceExpandToggle = (deviceId: ExtendedDevice["device_id"]): void => { if (expandedDeviceIds.includes(deviceId)) { @@ -338,6 +342,7 @@ const SessionManagerTab: React.FC = () => { versions={clientVersions} capabilities={capabilities} wellKnown={wellKnown} + authenticationConfig={authenticationConfig} /> diff --git a/yarn.lock b/yarn.lock index 423aee3a453..c763c32db10 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3052,6 +3052,27 @@ resolved "https://registry.yarnpkg.com/@zxcvbn-ts/language-en/-/language-en-3.0.2.tgz#162ada6b2b556444efd5a7700e70845cfde6d6ec" integrity sha512-Zp+zL+I6Un2Bj0tRXNs6VUBq3Djt+hwTwUz4dkt2qgsQz47U0/XthZ4ULrT/RxjwJRl5LwiaKOOZeOtmixHnjg== +"@zxing/browser@0.0.7": + version "0.0.7" + resolved "https://registry.yarnpkg.com/@zxing/browser/-/browser-0.0.7.tgz#5fa7680a867b660f48d3288fdf63e0174ad531c7" + integrity sha512-AepzMgDnD6EjxewqmXpHJsi4S3Gw9ilZJLIbTf6fWuWySEcHBodnGu3p7FWlgq1Sd5QyfPhTum5z3CBkkhMVng== + optionalDependencies: + "@zxing/text-encoding" "^0.9.0" + +"@zxing/library@^0.18.3": + version "0.18.6" + resolved "https://registry.yarnpkg.com/@zxing/library/-/library-0.18.6.tgz#717af8c6c1fd982865e21051afdd7b470ae6674c" + integrity sha512-bulZ9JHoLFd9W36pi+7e7DnEYNJhljYjZ1UTsKPOoLMU3qtC+REHITeCRNx40zTRJZx18W5TBRXt5pq2Uopjsw== + dependencies: + ts-custom-error "^3.0.0" + optionalDependencies: + "@zxing/text-encoding" "~0.9.0" + +"@zxing/text-encoding@^0.9.0", "@zxing/text-encoding@~0.9.0": + version "0.9.0" + resolved "https://registry.yarnpkg.com/@zxing/text-encoding/-/text-encoding-0.9.0.tgz#fb50ffabc6c7c66a0c96b4c03e3d9be74864b70b" + integrity sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA== + abab@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" @@ -7235,6 +7256,12 @@ oidc-client-ts@3.0.1, oidc-client-ts@^3.0.1: dependencies: jwt-decode "^4.0.0" +"oidc-client-ts@github:hughns/oidc-client-ts#hughns/device-flow": + version "3.0.1" + resolved "https://codeload.github.com/hughns/oidc-client-ts/tar.gz/456bdee9ca3c284e6626480e905987bfb79acbaf" + dependencies: + jwt-decode "^4.0.0" + on-finished@2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" @@ -7816,6 +7843,15 @@ react-is@^18.0.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== +react-qr-reader@^3.0.0-beta-1: + version "3.0.0-beta-1" + resolved "https://registry.yarnpkg.com/react-qr-reader/-/react-qr-reader-3.0.0-beta-1.tgz#e04a20876409313439959d8e0ea6df3ba6e36d68" + integrity sha512-5HeFH9x/BlziRYQYGK2AeWS9WiKYZtGGMs9DXy3bcySTX3C9UJL9EwcPnWw8vlf7JP4FcrAlr1SnZ5nsWLQGyw== + dependencies: + "@zxing/browser" "0.0.7" + "@zxing/library" "^0.18.3" + rollup "^2.67.2" + react-redux@^7.2.0: version "7.2.9" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.9.tgz#09488fbb9416a4efe3735b7235055442b042481d" @@ -8129,6 +8165,13 @@ rimraf@^5.0.0, rimraf@^5.0.5: dependencies: glob "^10.3.7" +rollup@^2.67.2: + version "2.79.1" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.79.1.tgz#bedee8faef7c9f93a2647ac0108748f497f081c7" + integrity sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw== + optionalDependencies: + fsevents "~2.3.2" + run-parallel@^1.1.9: version "1.2.0" resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" @@ -8846,6 +8889,11 @@ ts-api-utils@^1.0.1: resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.2.1.tgz#f716c7e027494629485b21c0df6180f4d08f5e8b" integrity sha512-RIYA36cJn2WiH9Hy77hdF9r7oEwxAtB/TS9/S4Qd90Ap4z5FSiin5zEiTL44OII1Y3IIlEvxwxFUVgrHSZ/UpA== +ts-custom-error@^3.0.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/ts-custom-error/-/ts-custom-error-3.3.1.tgz#8bd3c8fc6b8dc8e1cb329267c45200f1e17a65d1" + integrity sha512-5OX1tzOjxWEgsr/YEUWSuPrQ00deKLh6D7OTWcvNHm12/7QPyRh8SYpyWvA4IZv8H/+GQWQEh/kwo95Q9OVW1A== + ts-node@^10.9.1: version "10.9.2" resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f" From 40ae685275f3d85771cce3e368df301606b47f43 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 14 Mar 2024 14:53:37 +0000 Subject: [PATCH 02/98] Async import rendezvous bits as they include the rust crypto wasm blob Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .eslintrc.js | 4 ++-- src/components/views/auth/LoginWithQR.tsx | 23 +++++++++++------------ 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 3beb4d2332f..0b04b11f9ea 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -96,8 +96,8 @@ module.exports = { "!matrix-js-sdk/src/secret-storage", "!matrix-js-sdk/src/room-hierarchy", "!matrix-js-sdk/src/rendezvous", - "!matrix-js-sdk/src/rendezvous/transports", - "!matrix-js-sdk/src/rendezvous/channels", + "!matrix-js-sdk/src/rendezvous/RendezvousFailureReason", + "!matrix-js-sdk/src/rendezvous/RendezvousIntent", "!matrix-js-sdk/src/indexeddb-worker", "!matrix-js-sdk/src/pushprocessor", "!matrix-js-sdk/src/extensible_events_v1", diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index e23c1b20f31..27b90e394e4 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -15,19 +15,15 @@ limitations under the License. */ import React from "react"; -import { - MSC4108SignInWithQR, - RendezvousFailureReason, - RendezvousIntent, - buildLoginFromScannedCode, -} from "matrix-js-sdk/src/rendezvous"; -import { MSC4108RendezvousSession } from "matrix-js-sdk/src/rendezvous/transports"; -import { MSC4108SecureChannel } from "matrix-js-sdk/src/rendezvous/channels"; +// We import "matrix-js-sdk/src/rendezvous" asynchronously to avoid importing the entire Rust Crypto WASM into the main bundle. +import { RendezvousFailureReason } from "matrix-js-sdk/src/rendezvous/RendezvousFailureReason"; +import { RendezvousIntent } from "matrix-js-sdk/src/rendezvous/RendezvousIntent"; import { logger } from "matrix-js-sdk/src/logger"; import { MatrixClient, discoverAndValidateOIDCIssuerWellKnown, generateScope } from "matrix-js-sdk/src/matrix"; import { OnResultFunction } from "react-qr-reader"; import { OidcClient } from "oidc-client-ts"; +import type { MSC4108SignInWithQR } from "matrix-js-sdk/src/rendezvous"; import LoginWithQRFlow from "./LoginWithQRFlow"; import { getOidcClientId } from "../../../utils/oidc/registerClient"; import SdkConfig from "../../../SdkConfig"; @@ -171,19 +167,21 @@ export default class LoginWithQR extends React.Component { private generateAndShowCode = async (): Promise => { let rendezvous: MSC4108SignInWithQR; try { + const Rendezvous = await import("matrix-js-sdk/src/rendezvous"); + const fallbackRzServer = this.props.client?.getClientWellKnown()?.["io.element.rendezvous"]?.server ?? "https://rendezvous.lab.element.dev"; - const transport = new MSC4108RendezvousSession({ + const transport = new Rendezvous.MSC4108RendezvousSession({ onFailure: this.onFailure, client: this.props.client, fallbackRzServer, }); await transport.send(""); - const channel = new MSC4108SecureChannel(transport, undefined, this.onFailure); + const channel = new Rendezvous.MSC4108SecureChannel(transport, undefined, this.onFailure); - rendezvous = new MSC4108SignInWithQR(channel, false, this.props.client, this.onFailure); + rendezvous = new Rendezvous.MSC4108SignInWithQR(channel, false, this.props.client, this.onFailure); await rendezvous.generateCode(); this.setState({ @@ -231,6 +229,7 @@ export default class LoginWithQR extends React.Component { private processScannedCode = async (scannedCode: Buffer): Promise => { logger.info(scannedCode.toString()); try { + const Rendezvous = await import("matrix-js-sdk/src/rendezvous"); if (this.state.lastScannedCode?.equals(scannedCode)) { return; // suppress duplicate scans } @@ -240,7 +239,7 @@ export default class LoginWithQR extends React.Component { } const { signin: rendezvous, homeserverBaseUrl: homeserverBaseUrlFromCode } = - await buildLoginFromScannedCode(this.props.client, scannedCode, this.onFailure); + await Rendezvous.buildLoginFromScannedCode(this.props.client, scannedCode, this.onFailure); this.setState({ phase: Phase.Connecting, From 096f2f38be0c948486e6dbf94250c27661ba36d5 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 14 Mar 2024 18:01:34 +0000 Subject: [PATCH 03/98] Iterate PR Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../views/settings/devices/LoginWithQRSection.tsx | 9 ++++----- .../views/settings/tabs/user/SessionManagerTab.tsx | 14 ++++++++------ test/test-utils/oidc.ts | 1 + 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/components/views/settings/devices/LoginWithQRSection.tsx b/src/components/views/settings/devices/LoginWithQRSection.tsx index 74d007ed2d7..4ab638da01a 100644 --- a/src/components/views/settings/devices/LoginWithQRSection.tsx +++ b/src/components/views/settings/devices/LoginWithQRSection.tsx @@ -22,7 +22,6 @@ import { Capabilities, IClientWellKnown, OidcClientConfig, - AutoDiscoveryState, } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; @@ -35,7 +34,7 @@ interface IProps { versions?: IServerVersions; capabilities?: Capabilities; wellKnown?: IClientWellKnown; - authenticationConfig?: (OidcClientConfig & AutoDiscoveryState) | AutoDiscoveryState; + oidcClientConfig?: OidcClientConfig; } export default class LoginWithQRSection extends React.Component { @@ -56,9 +55,9 @@ export default class LoginWithQRSection extends React.Component { !!this.props.wellKnown?.["io.element.rendezvous"]?.server; const deviceAuthorizationGrantSupported = - this.props.authenticationConfig && - "metadata" in this.props.authenticationConfig && - this.props.authenticationConfig.metadata.grant_types_supported.includes( + this.props.oidcClientConfig && + "metadata" in this.props.oidcClientConfig && + this.props.oidcClientConfig.metadata.grant_types_supported.includes( "urn:ietf:params:oauth:grant-type:device_code", ); diff --git a/src/components/views/settings/tabs/user/SessionManagerTab.tsx b/src/components/views/settings/tabs/user/SessionManagerTab.tsx index 62c7c27ae3f..3618ca9794a 100644 --- a/src/components/views/settings/tabs/user/SessionManagerTab.tsx +++ b/src/components/views/settings/tabs/user/SessionManagerTab.tsx @@ -15,7 +15,7 @@ limitations under the License. */ import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; -import { AutoDiscovery, MatrixClient } from "matrix-js-sdk/src/matrix"; +import { discoverAndValidateOIDCIssuerWellKnown, MatrixClient } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; import { _t } from "../../../../../languageHandler"; @@ -184,10 +184,12 @@ const SessionManagerTab: React.FC = () => { const clientVersions = useAsyncMemo(() => matrixClient.getVersions(), [matrixClient]); const capabilities = useAsyncMemo(async () => matrixClient?.getCapabilities(), [matrixClient]); const wellKnown = useMemo(() => matrixClient?.getClientWellKnown(), [matrixClient]); - const authenticationConfig = useAsyncMemo( - async () => (wellKnown ? AutoDiscovery.discoverAndValidateAuthenticationConfig(wellKnown) : undefined), - [wellKnown], - ); + const oidcClientConfig = useAsyncMemo(async () => { + const authIssuer = await matrixClient?.getAuthIssuer(); + if (authIssuer) { + return discoverAndValidateOIDCIssuerWellKnown(authIssuer.issuer); + } + }, [matrixClient]); const onDeviceExpandToggle = (deviceId: ExtendedDevice["device_id"]): void => { if (expandedDeviceIds.includes(deviceId)) { @@ -342,7 +344,7 @@ const SessionManagerTab: React.FC = () => { versions={clientVersions} capabilities={capabilities} wellKnown={wellKnown} - authenticationConfig={authenticationConfig} + oidcClientConfig={oidcClientConfig} /> diff --git a/test/test-utils/oidc.ts b/test/test-utils/oidc.ts index 1a064fe5e70..c6d9b4b45d3 100644 --- a/test/test-utils/oidc.ts +++ b/test/test-utils/oidc.ts @@ -45,6 +45,7 @@ export const mockOpenIdConfiguration = (issuer = "https://auth.org/"): Validated token_endpoint: issuer + "token", authorization_endpoint: issuer + "auth", registration_endpoint: issuer + "registration", + device_authorization_endpoint: issuer + "device", jwks_uri: issuer + "jwks", response_types_supported: ["code"], grant_types_supported: ["authorization_code", "refresh_token"], From 74f762175f46c2476446980b3e2261f7823a1e96 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 18 Mar 2024 16:07:45 +0000 Subject: [PATCH 04/98] Switch to generating/parsing MSC4108 QR codes via Rust Crypto Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/auth/LoginWithQRFlow.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/auth/LoginWithQRFlow.tsx b/src/components/views/auth/LoginWithQRFlow.tsx index ce2ad80b672..f47395325c5 100644 --- a/src/components/views/auth/LoginWithQRFlow.tsx +++ b/src/components/views/auth/LoginWithQRFlow.tsx @@ -31,7 +31,7 @@ import { Click, Phase } from "./LoginWithQR"; interface IProps { phase: Phase; - code?: Buffer; + code?: Uint8Array; onClick(type: Click): Promise; failureReason?: RendezvousFailureReason; onScannedQRCode?: OnResultFunction; From 94dd62b73c43642c0343a21ffd38fcd0cb65b158 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 20 Mar 2024 13:59:40 +0000 Subject: [PATCH 05/98] Wait until secure channel is confirmed before doing OIDC registration The implementation was incorrect --- src/components/views/auth/LoginWithQR.tsx | 51 ++++++++++++----------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index 27b90e394e4..6d51a281702 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -196,26 +196,29 @@ export default class LoginWithQR extends React.Component { } try { - const { homeserverBaseUrl } = await rendezvous.loginStep1(); - if (this.state.ourIntent === RendezvousIntent.LOGIN_ON_NEW_DEVICE) { + // MSC4108-Flow: ExistingScanned + + // we get the homserver URL from the secure channel, but we don't trust it yet + const { homeserverBaseUrl } = await rendezvous.loginStep1(); + if (!homeserverBaseUrl) { throw new Error("We don't know the homeserver"); } - // PROTOTYPE: this feels bad, we have taken the homeserver URL that was sent by the other device - // before the channel was confirmed to the secure - // this could cause us to leak the OIDC registration data to a malicious server - // TODO: has this been implemented incorrectly? or is it a flaw in MSC4108? - await rendezvous.loginStep2(await this.getOidcClient(homeserverBaseUrl)); + this.setState({ + phase: Phase.OutOfBandConfirmation, + homeserverBaseUrl, + }); + } else { + // MSC4108-Flow: NewScanned + await rendezvous.loginStep1(); + const { verificationUri } = await rendezvous.loginStep2And3(); + this.setState({ + phase: Phase.OutOfBandConfirmation, + verificationUri, + }); } - const { verificationUri } = await rendezvous.loginStep3(); - this.setState({ - phase: Phase.OutOfBandConfirmation, - verificationUri, - homeserverBaseUrl, - }); - // we ask the user to confirm that the channel is secure } catch (e) { logger.error("Error whilst doing QR login", e); @@ -251,18 +254,14 @@ export default class LoginWithQR extends React.Component { const { homeserverBaseUrl: homeserverBaseUrlFromStep1 } = await rendezvous.loginStep1(); if (this.state.ourIntent === RendezvousIntent.LOGIN_ON_NEW_DEVICE) { + // MSC4108-Flow: NewScanned const homeserverBaseUrl = homeserverBaseUrlFromCode; if (!homeserverBaseUrl) { throw new Error("We don't know the homeserver"); } const oidcClient = await this.getOidcClient(homeserverBaseUrl); - await rendezvous.loginStep2(oidcClient); - - this.setState({ - phase: Phase.ShowChannelSecure, - }); - const { userCode } = await rendezvous.loginStep3(); + const { userCode } = await rendezvous.loginStep2And3(oidcClient); this.setState({ phase: Phase.ShowChannelSecure, userCode, @@ -290,11 +289,12 @@ export default class LoginWithQR extends React.Component { // done this.props.onFinished(credentials); } else { + // MSC4108-Flow: ExistingScanned const homeserverBaseUrl = homeserverBaseUrlFromStep1; this.setState({ phase: Phase.ShowChannelSecure, }); - const { verificationUri } = await rendezvous.loginStep3(); + const { verificationUri } = await rendezvous.loginStep2And3(); this.setState({ phase: Phase.Continue, verificationUri, @@ -313,6 +313,7 @@ export default class LoginWithQR extends React.Component { } if (this.state.ourIntent === RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE) { + // MSC4108-Flow: NewScanned this.setState({ phase: Phase.Loading }); if (this.state.verificationUri) { @@ -327,13 +328,13 @@ export default class LoginWithQR extends React.Component { // done this.props.onFinished(true); } else { - if (!this.state.homeserverBaseUrl) { + // MSC4108-Flow: ExistingScanned + const { homeserverBaseUrl } = this.state; + if (!homeserverBaseUrl) { throw new Error("We don't know the homeserver"); } - const { homeserverBaseUrl } = this.state; const oidcClient = await this.getOidcClient(homeserverBaseUrl); - await this.state.rendezvous.loginStep2(oidcClient); - const { userCode } = await this.state.rendezvous.loginStep3(); + const { userCode } = await this.state.rendezvous.loginStep2And3(oidcClient); this.setState({ phase: Phase.WaitingForDevice, userCode }); // wait for grant: From 8f6174ad564366713fa7c74cf6ea6ec2a70cdee0 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 22 Mar 2024 16:28:30 +0000 Subject: [PATCH 06/98] Wire up rust-crypto qr secrets import/export Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/Lifecycle.ts | 21 +++++++++++++++++---- src/MatrixClientPeg.ts | 2 +- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts index 8225ddb4ae2..0406c5e64e0 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -311,7 +311,12 @@ export async function completeDeviceAuthorizationGrant( logger.info("Logged in via OIDC Device Authorization Grant"); await onSuccessfulDelegatedAuthLogin(credentials); - const idTokenClaims = validateIdToken(idToken, oidcClient.settings.authority, oidcClient.settings.client_id, undefined) as IdTokenClaims; + const idTokenClaims = validateIdToken( + idToken, + oidcClient.settings.authority, + oidcClient.settings.client_id, + undefined, + ) as IdTokenClaims; persistOidcAuthenticatedSettings(oidcClient.settings.client_id, oidcClient.settings.authority, idTokenClaims); return { credentials }; } catch (error) { @@ -319,7 +324,8 @@ export async function completeDeviceAuthorizationGrant( await onFailedDelegatedAuthLogin(getOidcErrorMessage(error as Error)); return {}; - }} + } +} /** * Attempt to login by completing OIDC authorization code flow @@ -737,7 +743,10 @@ export async function setLoggedIn(credentials: IMatrixClientCreds): Promise { +export async function completeLoginWithQr( + credentials: IMatrixClientCreds, + secrets?: QRSecretsBundle, +): Promise { return doSetLoggedIn(credentials, false, secrets); } /** @@ -825,7 +834,11 @@ async function createOidcTokenRefresher(credentials: IMatrixClientCreds): Promis * * @returns {Promise} promise which resolves to the new MatrixClient once it has been started */ -async function doSetLoggedIn(credentials: IMatrixClientCreds, clearStorageEnabled: boolean, secrets?: QRSecretsBundle): Promise { +async function doSetLoggedIn( + credentials: IMatrixClientCreds, + clearStorageEnabled: boolean, + secrets?: QRSecretsBundle, +): Promise { checkSessionLock(); credentials.guest = Boolean(credentials.guest); diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index a0272a6cc0f..763b0ad4d11 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -33,6 +33,7 @@ import * as utils from "matrix-js-sdk/src/utils"; import { verificationMethods } from "matrix-js-sdk/src/crypto"; import { SHOW_QR_CODE_METHOD } from "matrix-js-sdk/src/crypto/verification/QRCode"; import { logger } from "matrix-js-sdk/src/logger"; +import { QRSecretsBundle } from "matrix-js-sdk/src/crypto-api"; import createMatrixClient from "./utils/createMatrixClient"; import SettingsStore from "./settings/SettingsStore"; @@ -54,7 +55,6 @@ import { formatList } from "./utils/FormattingUtils"; import SdkConfig from "./SdkConfig"; import { Features } from "./settings/Settings"; import { PhasedRolloutFeature } from "./utils/PhasedRolloutFeature"; -import { QRSecretsBundle } from "matrix-js-sdk/src/crypto-api"; export interface IMatrixClientCreds { homeserverUrl: string; From fbfb0f4729cc10450f13c668d06e5966a51df74c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 25 Mar 2024 15:44:54 +0000 Subject: [PATCH 07/98] Shuttle secrets via credentials and avoid calling doSetLoggedIn twice Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/Lifecycle.ts | 8 ++------ src/MatrixClientPeg.ts | 1 + src/components/views/auth/LoginWithQR.tsx | 10 +++------- 3 files changed, 6 insertions(+), 13 deletions(-) diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts index 07ab1f81f70..827e58c33f7 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -798,11 +798,7 @@ async function createOidcTokenRefresher(credentials: IMatrixClientCreds): Promis * * @returns {Promise} promise which resolves to the new MatrixClient once it has been started */ -async function doSetLoggedIn( - credentials: IMatrixClientCreds, - clearStorageEnabled: boolean, - secrets?: QRSecretsBundle, -): Promise { +async function doSetLoggedIn(credentials: IMatrixClientCreds, clearStorageEnabled: boolean): Promise { checkSessionLock(); credentials.guest = Boolean(credentials.guest); @@ -873,7 +869,7 @@ async function doSetLoggedIn( checkSessionLock(); dis.fire(Action.OnLoggedIn); - await startMatrixClient(client, /*startSyncing=*/ !softLogout, secrets); + await startMatrixClient(client, /*startSyncing=*/ !softLogout, credentials.secrets); return client; } diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index 763b0ad4d11..b7bdacb0855 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -66,6 +66,7 @@ export interface IMatrixClientCreds { guest?: boolean; pickleKey?: string; freshLogin?: boolean; + secrets?: QRSecretsBundle; } /** diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index 6d51a281702..32a0a117231 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -27,7 +27,7 @@ import type { MSC4108SignInWithQR } from "matrix-js-sdk/src/rendezvous"; import LoginWithQRFlow from "./LoginWithQRFlow"; import { getOidcClientId } from "../../../utils/oidc/registerClient"; import SdkConfig from "../../../SdkConfig"; -import { completeDeviceAuthorizationGrant, completeLoginWithQr } from "../../../Lifecycle"; +import { completeDeviceAuthorizationGrant } from "../../../Lifecycle"; /** * The intention of this enum is to have a mode that scans a QR code instead of generating one. @@ -284,10 +284,8 @@ export default class LoginWithQR extends React.Component { // wait for secrets: const { secrets } = await rendezvous.loginStep5(credentials.deviceId); - await completeLoginWithQr(credentials, secrets); - // done - this.props.onFinished(credentials); + this.props.onFinished({ ...credentials, secrets }); } else { // MSC4108-Flow: ExistingScanned const homeserverBaseUrl = homeserverBaseUrlFromStep1; @@ -354,10 +352,8 @@ export default class LoginWithQR extends React.Component { // wait for secrets const { secrets } = await this.state.rendezvous.loginStep5(credentials.deviceId); - await completeLoginWithQr(credentials, secrets); - // done - this.props.onFinished(credentials); + this.props.onFinished({ ...credentials, secrets }); } }; From 62d3e7a92bade568c118cead413719be6d1a7d82 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 25 Mar 2024 15:45:11 +0000 Subject: [PATCH 08/98] Restore key backup after importing QR secrets Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/MatrixClientPeg.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index b7bdacb0855..d427b2ca929 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -340,6 +340,12 @@ class MatrixClientPegClass implements IMatrixClientPeg { if (secrets) { this.matrixClient.getCrypto()?.importSecretsForQRLogin(secrets); + if (secrets.backup) { + const backupInfo = await this.matrixClient.getKeyBackupVersion(); + if (backupInfo) { + await this.matrixClient.restoreKeyBackupWithCache(undefined, undefined, backupInfo, {}); + } + } } StorageManager.setCryptoInitialised(true); From 02154ab87fbfbf127289f521579310981c3f62f0 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 25 Mar 2024 16:34:26 +0000 Subject: [PATCH 09/98] Remove unused method Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/Lifecycle.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts index 827e58c33f7..018aa8caffe 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -707,12 +707,6 @@ export async function setLoggedIn(credentials: IMatrixClientCreds): Promise { - return doSetLoggedIn(credentials, false, secrets); -} /** * Hydrates an existing session by using the credentials provided. This will * not clear any local storage, unlike setLoggedIn(). From b4cd325a54f212da60c55950ce29d02ebf81b281 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 25 Mar 2024 16:34:48 +0000 Subject: [PATCH 10/98] Delint Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/auth/LoginWithQRFlow.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/auth/LoginWithQRFlow.tsx b/src/components/views/auth/LoginWithQRFlow.tsx index 582a294c45b..a4a5dd87693 100644 --- a/src/components/views/auth/LoginWithQRFlow.tsx +++ b/src/components/views/auth/LoginWithQRFlow.tsx @@ -25,7 +25,7 @@ import AccessibleButton from "../elements/AccessibleButton"; import QRCode from "../elements/QRCode"; import Spinner from "../elements/Spinner"; import { Icon as CheckmarkIcon } from "../../../../res/img/element-icons/check.svg"; -import { Click, FailureReason, LoginWithQRFailureReason, Phase } from "./LoginWithQR"; +import { Click, Phase } from "./LoginWithQR"; import SdkConfig from "../../../SdkConfig"; interface IProps { From 6054a71d1ae099b4c81a728fca3d321dfe80f041 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 25 Mar 2024 16:38:10 +0000 Subject: [PATCH 11/98] i18n Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/i18n/strings/en_EN.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 31ff6cf91dc..96913469bf5 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -245,9 +245,7 @@ "phone_label": "Phone", "phone_optional_label": "Phone (optional)", "qr_code_login": { - "approve_access_warning": "By approving access for this device, it will have full access to your account.", "completing_setup": "Completing set up of your new device", - "confirm_code_match": "Check that the code below matches with your other device:", "connecting": "Connecting…", "error_device_already_signed_in": "The other device is already signed in.", "error_device_not_signed_in": "The other device isn't signed in.", @@ -255,7 +253,6 @@ "error_homeserver_lacks_support": "The homeserver doesn't support signing in another device.", "error_invalid_scanned_code": "The scanned code is invalid.", "error_linking_incomplete": "The linking wasn't completed in the required time.", - "error_rate_limited": "Too many attempts in a short time. Wait some time before trying again.", "error_request_cancelled": "The request was cancelled.", "error_request_declined": "The request was declined on the other device.", "error_unexpected": "An unexpected error occurred.", @@ -265,7 +262,6 @@ "scan_code_instruction": "Scan the QR code with another device", "scan_qr_code": "Scan QR code", "select_qr_code": "Select \"%(scanQRCode)s\"", - "sign_in_new_device": "Sign in new device", "waiting_for_device": "Waiting for device to sign in" }, "register_action": "Create Account", From 2b8ffa37027ea27f016b3cd15f1aa4127bb2b8db Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 25 Mar 2024 17:51:12 +0000 Subject: [PATCH 12/98] Ensure rust crypto wasm is loaded async Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/auth/LoginWithQR.tsx | 25 ++++++++++++++--------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index 32a0a117231..bafca63ca88 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { lazy, Suspense } from "react"; // We import "matrix-js-sdk/src/rendezvous" asynchronously to avoid importing the entire Rust Crypto WASM into the main bundle. import { RendezvousFailureReason } from "matrix-js-sdk/src/rendezvous/RendezvousFailureReason"; import { RendezvousIntent } from "matrix-js-sdk/src/rendezvous/RendezvousIntent"; @@ -24,10 +24,13 @@ import { OnResultFunction } from "react-qr-reader"; import { OidcClient } from "oidc-client-ts"; import type { MSC4108SignInWithQR } from "matrix-js-sdk/src/rendezvous"; -import LoginWithQRFlow from "./LoginWithQRFlow"; import { getOidcClientId } from "../../../utils/oidc/registerClient"; import SdkConfig from "../../../SdkConfig"; import { completeDeviceAuthorizationGrant } from "../../../Lifecycle"; +import Spinner from "../elements/Spinner"; + +// We import `LoginWithQRFlow` asynchronously to avoid importing the entire Rust Crypto WASM into the main bundle. +const LoginWithQRFlow = lazy(() => import("./LoginWithQRFlow")); /** * The intention of this enum is to have a mode that scans a QR code instead of generating one. @@ -424,14 +427,16 @@ export default class LoginWithQR extends React.Component { public render(): React.ReactNode { logger.info("LoginWithQR render"); return ( - + }> + + ); } } From 887a2b9d0ac4793b446c62790904260a8e9d0a25 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 25 Mar 2024 18:19:37 +0000 Subject: [PATCH 13/98] Remove changes which rely on major oidc-client-ts upstream changes Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- package.json | 7 +- src/Lifecycle.ts | 44 +---- src/components/views/auth/LoginWithQR.tsx | 162 +----------------- src/components/views/auth/LoginWithQRFlow.tsx | 44 ----- yarn.lock | 48 ------ 5 files changed, 9 insertions(+), 296 deletions(-) diff --git a/package.json b/package.json index af88c183d44..271208c8798 100644 --- a/package.json +++ b/package.json @@ -112,7 +112,7 @@ "matrix-widget-api": "^1.5.0", "memoize-one": "^6.0.0", "minimist": "^1.2.5", - "oidc-client-ts": "github:hughns/oidc-client-ts#hughns/device-flow", + "oidc-client-ts": "^3.0.1", "opus-recorder": "^8.0.3", "pako": "^2.0.3", "png-chunks-extract": "^1.0.0", @@ -132,8 +132,7 @@ "tar-js": "^0.3.0", "ua-parser-js": "^1.0.2", "uuid": "^9.0.0", - "what-input": "^5.2.10", - "react-qr-reader": "^3.0.0-beta-1" + "what-input": "^5.2.10" }, "devDependencies": { "@action-validator/cli": "^0.6.0", @@ -235,4 +234,4 @@ "outputName": "jest-sonar-report.xml", "relativePaths": true } -} \ No newline at end of file +} diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts index 018aa8caffe..31922bbf1ef 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -18,11 +18,10 @@ limitations under the License. */ import { ReactNode } from "react"; -import { createClient, MatrixClient, SSOAction, OidcTokenRefresher, validateIdToken } from "matrix-js-sdk/src/matrix"; +import { createClient, MatrixClient, SSOAction, OidcTokenRefresher } from "matrix-js-sdk/src/matrix"; import { IEncryptedPayload } from "matrix-js-sdk/src/crypto/aes"; import { QueryDict } from "matrix-js-sdk/src/utils"; import { logger } from "matrix-js-sdk/src/logger"; -import { DeviceAccessTokenResponse, IdTokenClaims, OidcClient } from "oidc-client-ts"; import { QRSecretsBundle } from "matrix-js-sdk/src/crypto-api"; import { IMatrixClientCreds, MatrixClientPeg } from "./MatrixClientPeg"; @@ -283,47 +282,6 @@ export async function attemptDelegatedAuthLogin( return attemptTokenLogin(queryParams, defaultDeviceDisplayName, fragmentAfterLogin); } -export async function completeDeviceAuthorizationGrant( - oidcClient: OidcClient, - { access_token: accessToken, refresh_token: refreshToken, id_token: idToken }: DeviceAccessTokenResponse, - homeserverUrl: string, - identityServerUrl?: string, -): Promise<{ credentials?: IMatrixClientCreds }> { - try { - const { - user_id: userId, - device_id: deviceId, - is_guest: isGuest, - } = await getUserIdFromAccessToken(accessToken, homeserverUrl, identityServerUrl); - - const credentials = { - accessToken, - refreshToken, - homeserverUrl, - identityServerUrl, - deviceId, - userId, - isGuest, - }; - - logger.info("Logged in via OIDC Device Authorization Grant"); - await onSuccessfulDelegatedAuthLogin(credentials); - const idTokenClaims = validateIdToken( - idToken, - oidcClient.settings.authority, - oidcClient.settings.client_id, - undefined, - ) as IdTokenClaims; - persistOidcAuthenticatedSettings(oidcClient.settings.client_id, oidcClient.settings.authority, idTokenClaims); - return { credentials }; - } catch (error) { - logger.error("Failed to login via OIDC Device Authorization Grant", error); - - await onFailedDelegatedAuthLogin(getOidcErrorMessage(error as Error)); - return {}; - } -} - /** * Attempt to login by completing OIDC authorization code flow * @param queryParams string->string map of the query-parameters extracted from the real query-string of the starting URI. diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index bafca63ca88..b1f373b5df3 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -19,14 +19,9 @@ import React, { lazy, Suspense } from "react"; import { RendezvousFailureReason } from "matrix-js-sdk/src/rendezvous/RendezvousFailureReason"; import { RendezvousIntent } from "matrix-js-sdk/src/rendezvous/RendezvousIntent"; import { logger } from "matrix-js-sdk/src/logger"; -import { MatrixClient, discoverAndValidateOIDCIssuerWellKnown, generateScope } from "matrix-js-sdk/src/matrix"; -import { OnResultFunction } from "react-qr-reader"; -import { OidcClient } from "oidc-client-ts"; +import { MatrixClient } from "matrix-js-sdk/src/matrix"; import type { MSC4108SignInWithQR } from "matrix-js-sdk/src/rendezvous"; -import { getOidcClientId } from "../../../utils/oidc/registerClient"; -import SdkConfig from "../../../SdkConfig"; -import { completeDeviceAuthorizationGrant } from "../../../Lifecycle"; import Spinner from "../elements/Spinner"; // We import `LoginWithQRFlow` asynchronously to avoid importing the entire Rust Crypto WASM into the main bundle. @@ -40,12 +35,12 @@ export enum Mode { * A QR code with be generated and shown */ Show = "show", - Scan = "scan", + // Scan = "scan", } export enum Phase { Loading = "loading", - ScanningQR = "scanningQR", + // ScanningQR = "scanningQR", ShowingQR = "showingQR", Connecting = "connecting", OutOfBandConfirmation = "outOfBandConfirmation", @@ -62,7 +57,7 @@ export enum Click { Approve, TryAgain, Back, - ScanQr, + // ScanQr, ShowQr, } @@ -130,9 +125,6 @@ export default class LoginWithQR extends React.Component { } if (mode === Mode.Show) { await this.generateAndShowCode(); - } else { - await this.requestMediaPermissions(); - this.setState({ phase: Phase.ScanningQR }); } } @@ -145,28 +137,6 @@ export default class LoginWithQR extends React.Component { } } - private getOidcClient = async (homeserverBaseUrl: string): Promise => { - // oidc discovery - const tempClient = new MatrixClient({ baseUrl: homeserverBaseUrl }); - // this should fall back to the well-known - const { issuer } = await tempClient.getAuthIssuer(); - // AutoDiscovery; - const metadata = await discoverAndValidateOIDCIssuerWellKnown(issuer); - // oidc registration - const clientId = await getOidcClientId(metadata, SdkConfig.get().oidc_static_clients); - - const scope = generateScope(); - const oidcClient = new OidcClient({ - ...metadata, - client_id: clientId, - redirect_uri: window.location.href, - authority: issuer, - scope, - }); - - return oidcClient; - }; - private generateAndShowCode = async (): Promise => { let rendezvous: MSC4108SignInWithQR; try { @@ -232,82 +202,6 @@ export default class LoginWithQR extends React.Component { } }; - private processScannedCode = async (scannedCode: Buffer): Promise => { - logger.info(scannedCode.toString()); - try { - const Rendezvous = await import("matrix-js-sdk/src/rendezvous"); - if (this.state.lastScannedCode?.equals(scannedCode)) { - return; // suppress duplicate scans - } - if (this.state.rendezvous) { - await this.state.rendezvous.cancel(RendezvousFailureReason.UserCancelled); - this.reset(); - } - - const { signin: rendezvous, homeserverBaseUrl: homeserverBaseUrlFromCode } = - await Rendezvous.buildLoginFromScannedCode(this.props.client, scannedCode, this.onFailure); - - this.setState({ - phase: Phase.Connecting, - lastScannedCode: scannedCode, - rendezvous, - failureReason: undefined, - }); - - const { homeserverBaseUrl: homeserverBaseUrlFromStep1 } = await rendezvous.loginStep1(); - - if (this.state.ourIntent === RendezvousIntent.LOGIN_ON_NEW_DEVICE) { - // MSC4108-Flow: NewScanned - const homeserverBaseUrl = homeserverBaseUrlFromCode; - - if (!homeserverBaseUrl) { - throw new Error("We don't know the homeserver"); - } - const oidcClient = await this.getOidcClient(homeserverBaseUrl); - const { userCode } = await rendezvous.loginStep2And3(oidcClient); - this.setState({ - phase: Phase.ShowChannelSecure, - userCode, - }); - - // wait for grant: - const tokenResponse = await rendezvous.loginStep4(); - - const { credentials } = await completeDeviceAuthorizationGrant( - oidcClient, - tokenResponse, - homeserverBaseUrl, - undefined, - ); - - if (!credentials) { - throw new Error("Failed to complete device authorization grant"); - } - - // wait for secrets: - const { secrets } = await rendezvous.loginStep5(credentials.deviceId); - - // done - this.props.onFinished({ ...credentials, secrets }); - } else { - // MSC4108-Flow: ExistingScanned - const homeserverBaseUrl = homeserverBaseUrlFromStep1; - this.setState({ - phase: Phase.ShowChannelSecure, - }); - const { verificationUri } = await rendezvous.loginStep2And3(); - this.setState({ - phase: Phase.Continue, - verificationUri, - homeserverBaseUrl, - }); - } - } catch (e) { - alert(e); - throw e; - } - }; - private approveLoginAfterShowingCode = async (): Promise => { if (!this.state.rendezvous) { throw new Error("Rendezvous not found"); @@ -329,34 +223,7 @@ export default class LoginWithQR extends React.Component { // done this.props.onFinished(true); } else { - // MSC4108-Flow: ExistingScanned - const { homeserverBaseUrl } = this.state; - if (!homeserverBaseUrl) { - throw new Error("We don't know the homeserver"); - } - const oidcClient = await this.getOidcClient(homeserverBaseUrl); - const { userCode } = await this.state.rendezvous.loginStep2And3(oidcClient); - this.setState({ phase: Phase.WaitingForDevice, userCode }); - - // wait for grant: - const tokenResponse = await this.state.rendezvous.loginStep4(); - - const { credentials } = await completeDeviceAuthorizationGrant( - oidcClient, - tokenResponse, - homeserverBaseUrl, - undefined, - ); - - if (!credentials) { - throw new Error("Failed to complete device authorization grant"); - } - - // wait for secrets - const { secrets } = await this.state.rendezvous.loginStep5(credentials.deviceId); - - // done - this.props.onFinished({ ...credentials, secrets }); + throw new Error("New device flows around OIDC are not yet implemented"); } }; @@ -400,30 +267,12 @@ export default class LoginWithQR extends React.Component { await this.state.rendezvous?.cancel(RendezvousFailureReason.UserCancelled); this.props.onFinished(false); break; - case Click.ScanQr: - await this.updateMode(Mode.Scan); - break; case Click.ShowQr: await this.updateMode(Mode.Show); break; } }; - private onScannedQRCode: OnResultFunction = (result, error): void => { - if (result) { - void this.processScannedCode(Buffer.from((result.getResultMetadata().get(2) as [Uint8Array])[0])); - } - }; - - private requestMediaPermissions = async (): Promise => { - try { - await navigator.mediaDevices.getUserMedia({ video: true }); - this.setState({ mediaPermissionError: false }); - } catch (err) { - this.setState({ mediaPermissionError: true }); - } - }; - public render(): React.ReactNode { logger.info("LoginWithQR render"); return ( @@ -433,7 +282,6 @@ export default class LoginWithQR extends React.Component { phase={this.state.phase} code={this.state.phase === Phase.ShowingQR ? this.state.rendezvous?.code : undefined} failureReason={this.state.phase === Phase.Error ? this.state.failureReason : undefined} - onScannedQRCode={this.onScannedQRCode} userCode={this.state.userCode} /> diff --git a/src/components/views/auth/LoginWithQRFlow.tsx b/src/components/views/auth/LoginWithQRFlow.tsx index a4a5dd87693..c733b34c098 100644 --- a/src/components/views/auth/LoginWithQRFlow.tsx +++ b/src/components/views/auth/LoginWithQRFlow.tsx @@ -17,7 +17,6 @@ limitations under the License. import React from "react"; import { RendezvousFailureReason } from "matrix-js-sdk/src/rendezvous"; import { Icon as ChevronLeftIcon } from "@vector-im/compound-design-tokens/icons/chevron-left.svg"; -import { QrReader, OnResultFunction } from "react-qr-reader"; import { logger } from "matrix-js-sdk/src/logger"; import { _t } from "../../../languageHandler"; @@ -33,7 +32,6 @@ interface IProps { code?: Uint8Array; onClick(type: Click): Promise; failureReason?: RendezvousFailureReason; - onScannedQRCode?: OnResultFunction; userCode?: string; } @@ -71,17 +69,6 @@ export default class LoginWithQRFlow extends React.Component { ); }; - private viewFinder(): JSX.Element { - return ( - - - - - - - ); - } - public render(): React.ReactNode { logger.info(`LoginWithQRFlow render: phase=${this.props.phase}`); let main: JSX.Element | undefined; @@ -262,15 +249,6 @@ export default class LoginWithQRFlow extends React.Component { ); - buttons = ( - - Scan QR code instead - - ); } else { main = this.simpleSpinner(); buttons = this.cancelButton(); @@ -302,28 +280,6 @@ export default class LoginWithQRFlow extends React.Component { centreTitle = true; main = this.simpleSpinner(_t("auth|qr_code_login|completing_setup")); break; - case Phase.ScanningQR: - main = ( - <> -

Line up the QR code in the square below:

- - - ); - buttons = ( - - Show QR code instead - - ); - break; } return ( diff --git a/yarn.lock b/yarn.lock index 60ae317c465..ea4f6367def 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3074,27 +3074,6 @@ resolved "https://registry.yarnpkg.com/@zxcvbn-ts/language-en/-/language-en-3.0.2.tgz#162ada6b2b556444efd5a7700e70845cfde6d6ec" integrity sha512-Zp+zL+I6Un2Bj0tRXNs6VUBq3Djt+hwTwUz4dkt2qgsQz47U0/XthZ4ULrT/RxjwJRl5LwiaKOOZeOtmixHnjg== -"@zxing/browser@0.0.7": - version "0.0.7" - resolved "https://registry.yarnpkg.com/@zxing/browser/-/browser-0.0.7.tgz#5fa7680a867b660f48d3288fdf63e0174ad531c7" - integrity sha512-AepzMgDnD6EjxewqmXpHJsi4S3Gw9ilZJLIbTf6fWuWySEcHBodnGu3p7FWlgq1Sd5QyfPhTum5z3CBkkhMVng== - optionalDependencies: - "@zxing/text-encoding" "^0.9.0" - -"@zxing/library@^0.18.3": - version "0.18.6" - resolved "https://registry.yarnpkg.com/@zxing/library/-/library-0.18.6.tgz#717af8c6c1fd982865e21051afdd7b470ae6674c" - integrity sha512-bulZ9JHoLFd9W36pi+7e7DnEYNJhljYjZ1UTsKPOoLMU3qtC+REHITeCRNx40zTRJZx18W5TBRXt5pq2Uopjsw== - dependencies: - ts-custom-error "^3.0.0" - optionalDependencies: - "@zxing/text-encoding" "~0.9.0" - -"@zxing/text-encoding@^0.9.0", "@zxing/text-encoding@~0.9.0": - version "0.9.0" - resolved "https://registry.yarnpkg.com/@zxing/text-encoding/-/text-encoding-0.9.0.tgz#fb50ffabc6c7c66a0c96b4c03e3d9be74864b70b" - integrity sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA== - abab@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" @@ -7389,12 +7368,6 @@ oidc-client-ts@3.0.1, oidc-client-ts@^3.0.1: dependencies: jwt-decode "^4.0.0" -"oidc-client-ts@github:hughns/oidc-client-ts#hughns/device-flow": - version "3.0.1" - resolved "https://codeload.github.com/hughns/oidc-client-ts/tar.gz/456bdee9ca3c284e6626480e905987bfb79acbaf" - dependencies: - jwt-decode "^4.0.0" - on-finished@2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" @@ -7976,15 +7949,6 @@ react-is@^18.0.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== -react-qr-reader@^3.0.0-beta-1: - version "3.0.0-beta-1" - resolved "https://registry.yarnpkg.com/react-qr-reader/-/react-qr-reader-3.0.0-beta-1.tgz#e04a20876409313439959d8e0ea6df3ba6e36d68" - integrity sha512-5HeFH9x/BlziRYQYGK2AeWS9WiKYZtGGMs9DXy3bcySTX3C9UJL9EwcPnWw8vlf7JP4FcrAlr1SnZ5nsWLQGyw== - dependencies: - "@zxing/browser" "0.0.7" - "@zxing/library" "^0.18.3" - rollup "^2.67.2" - react-redux@^7.2.0: version "7.2.9" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.9.tgz#09488fbb9416a4efe3735b7235055442b042481d" @@ -8298,13 +8262,6 @@ rimraf@^5.0.0, rimraf@^5.0.5: dependencies: glob "^10.3.7" -rollup@^2.67.2: - version "2.79.1" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.79.1.tgz#bedee8faef7c9f93a2647ac0108748f497f081c7" - integrity sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw== - optionalDependencies: - fsevents "~2.3.2" - run-parallel@^1.1.9: version "1.2.0" resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" @@ -9067,11 +9024,6 @@ ts-api-utils@^1.0.1: resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz#4b490e27129f1e8e686b45cc4ab63714dc60eea1" integrity sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ== -ts-custom-error@^3.0.0: - version "3.3.1" - resolved "https://registry.yarnpkg.com/ts-custom-error/-/ts-custom-error-3.3.1.tgz#8bd3c8fc6b8dc8e1cb329267c45200f1e17a65d1" - integrity sha512-5OX1tzOjxWEgsr/YEUWSuPrQ00deKLh6D7OTWcvNHm12/7QPyRh8SYpyWvA4IZv8H/+GQWQEh/kwo95Q9OVW1A== - ts-node@^10.9.1: version "10.9.2" resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f" From 99b5213cfbc88f0de30c4762746b3bf042337d37 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 25 Mar 2024 18:26:41 +0000 Subject: [PATCH 14/98] Prettier Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- res/css/structures/auth/_Login.pcss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/structures/auth/_Login.pcss b/res/css/structures/auth/_Login.pcss index fe5e4bc9ad0..225dfd25070 100644 --- a/res/css/structures/auth/_Login.pcss +++ b/res/css/structures/auth/_Login.pcss @@ -109,7 +109,7 @@ div.mx_AccessibleButton_kind_link.mx_Login_forgot { font-size: 14px; text-align: center; margin-top: -14px; - color: #737D8C; + color: #737d8c; margin-bottom: 10px; } From fda2898e31ca8c8eef8c42c0daa76263b2c3c706 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 26 Mar 2024 08:54:22 +0000 Subject: [PATCH 15/98] Remove login qr flows Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- res/css/structures/auth/_Login.pcss | 19 ------- src/Login.ts | 3 +- src/components/structures/auth/Login.tsx | 64 ++++++----------------- src/components/views/auth/LoginWithQR.tsx | 41 +++++++++++---- 4 files changed, 47 insertions(+), 80 deletions(-) diff --git a/res/css/structures/auth/_Login.pcss b/res/css/structures/auth/_Login.pcss index 225dfd25070..aa4244bcfbd 100644 --- a/res/css/structures/auth/_Login.pcss +++ b/res/css/structures/auth/_Login.pcss @@ -104,22 +104,3 @@ div.mx_AccessibleButton_kind_link.mx_Login_forgot { width: 100%; margin-bottom: 16px; } - -.mx_Login_withQR_or { - font-size: 14px; - text-align: center; - margin-top: -14px; - color: #737d8c; - margin-bottom: 10px; -} - -.mx_Login_withQR { - display: block !important; - margin-bottom: 20px; - svg { - height: 1em; - vertical-align: middle; - margin-right: 10px; - padding-bottom: 2px; - } -} diff --git a/src/Login.ts b/src/Login.ts index 3d99548d942..4f198fc634e 100644 --- a/src/Login.ts +++ b/src/Login.ts @@ -123,7 +123,7 @@ export default class Login { SdkConfig.get().oidc_static_clients, isRegistration, ); - return [oidcFlow, { type: "loginWithQR" }]; // PROTOTYPE: this should probably be behind a feature flag + return [oidcFlow]; } catch (error) { logger.error(error); } @@ -138,7 +138,6 @@ export default class Login { (f) => f.type === "m.login.sso" && DELEGATED_OIDC_COMPATIBILITY.findIn(f), ); this.flows = oidcCompatibilityFlow ? [oidcCompatibilityFlow] : flows; - this.flows.push({ type: "loginWithQR" }); // PROTOTYPE: this should probably be behind a feature flag return this.flows; } diff --git a/src/components/structures/auth/Login.tsx b/src/components/structures/auth/Login.tsx index c74643a0c08..11b843d2218 100644 --- a/src/components/structures/auth/Login.tsx +++ b/src/components/structures/auth/Login.tsx @@ -40,8 +40,6 @@ import { ValidatedServerConfig } from "../../../utils/ValidatedServerConfig"; import { filterBoolean } from "../../../utils/arrays"; import { Features } from "../../../settings/Settings"; import { startOidcLogin } from "../../../utils/oidc/authorize"; -import LoginWithQR, { Mode } from "../../views/auth/LoginWithQR"; -import { Icon as QRIcon } from "../../../../res/img/element-icons/qrcode.svg"; interface IProps { serverConfig: ValidatedServerConfig; @@ -142,7 +140,6 @@ export default class LoginComponent extends React.PureComponent // eslint-disable-next-line @typescript-eslint/naming-convention "m.login.sso": () => this.renderSsoStep("sso"), "oidcNativeFlow": () => this.renderOidcNativeStep(), - "loginWithQR": () => this.renderLoginWithQRStep(), }; } @@ -426,7 +423,7 @@ export default class LoginComponent extends React.PureComponent if (!this.state.flows) return null; // this is the ideal order we want to show the flows in - const order = ["loginWithQR", "oidcNativeFlow", "m.login.password", "m.login.sso"]; + const order = ["oidcNativeFlow", "m.login.password", "m.login.sso"]; const flows = filterBoolean(order.map((type) => this.state.flows?.find((flow) => flow.type === type))); return ( @@ -494,31 +491,6 @@ export default class LoginComponent extends React.PureComponent ); }; - private startLoginWithQR = (): void => { - this.setState({ loginWithQrInProgress: true }); - }; - - private renderLoginWithQRStep = (): JSX.Element | null => { - return ( - <> -

or

- - - Sign in with QR code - - - ); - }; - - private onLoginWithQRFinished = (data: IMatrixClientCreds): void => { - if (!data) { - this.setState({ loginWithQrInProgress: false }); - } else { - // PROTOTYPE: I'm not sure if this is the right way to do this. Obviously we don't have a password - this.props.onLoggedIn(data, ""); - } - }; - public render(): React.ReactNode { const loader = this.isBusy() && !this.state.busyLoggingIn ? ( @@ -578,26 +550,20 @@ export default class LoginComponent extends React.PureComponent return ( - {this.state.loginWithQrInProgress ? ( - - - - ) : ( - -

- {_t("action|sign_in")} - {loader} -

- {errorTextSection} - {serverDeadSection} - - {this.renderLoginComponentForFlows()} - {footer} -
- )} + +

+ {_t("action|sign_in")} + {loader} +

+ {errorTextSection} + {serverDeadSection} + + {this.renderLoginComponentForFlows()} + {footer} +
); } diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index b1f373b5df3..1b1a8688712 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -39,18 +39,33 @@ export enum Mode { } export enum Phase { - Loading = "loading", - // ScanningQR = "scanningQR", - ShowingQR = "showingQR", - Connecting = "connecting", - OutOfBandConfirmation = "outOfBandConfirmation", - ShowChannelSecure = "showChannelSecure", - WaitingForDevice = "waitingForDevice", - Verifying = "verifying", - Continue = "continue", - Error = "error", + Loading, + ShowingQR, + Connecting, + /** + * @deprecated the MSC3906 implementation is deprecated in favour of MSC4108. + */ + Connected, + OutOfBandConfirmation, + ShowChannelSecure, + WaitingForDevice, + Verifying, + Continue, + Error, } +/** + * @deprecated the MSC3906 implementation is deprecated in favour of MSC4108. + */ +export type LegacyPhase = + | Phase.Loading + | Phase.ShowingQR + | Phase.Connecting + | Phase.Connected + | Phase.WaitingForDevice + | Phase.Verifying + | Phase.Error; + export enum Click { Cancel, Decline, @@ -79,10 +94,16 @@ interface IState { homeserverBaseUrl?: string; } +/** + * @deprecated the MSC3906 implementation is deprecated in favour of MSC4108. + */ export enum LoginWithQRFailureReason { RateLimited = "rate_limited", } +/** + * @deprecated the MSC3906 implementation is deprecated in favour of MSC4108. See {@see RendezvousFailureReason}. + */ export type FailureReason = RendezvousFailureReason | LoginWithQRFailureReason; /** From a7ba73b1a4b224aa63a2e487284fd9142d38b0af Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 26 Mar 2024 09:07:01 +0000 Subject: [PATCH 16/98] Simplify Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .eslintrc.js | 2 - .../views/auth/LoginWithQR-types.ts | 64 +++++++++++++ src/components/views/auth/LoginWithQR.tsx | 92 ++++--------------- src/components/views/auth/LoginWithQRFlow.tsx | 2 +- .../settings/tabs/user/SessionManagerTab.tsx | 14 ++- .../settings/devices/LoginWithQR-test.tsx | 3 +- .../settings/devices/LoginWithQRFlow-test.tsx | 8 +- 7 files changed, 100 insertions(+), 85 deletions(-) create mode 100644 src/components/views/auth/LoginWithQR-types.ts diff --git a/.eslintrc.js b/.eslintrc.js index cba0bc9d49d..03f7cbbaf5b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -98,8 +98,6 @@ module.exports = { "!matrix-js-sdk/src/secret-storage", "!matrix-js-sdk/src/room-hierarchy", "!matrix-js-sdk/src/rendezvous", - "!matrix-js-sdk/src/rendezvous/RendezvousFailureReason", - "!matrix-js-sdk/src/rendezvous/RendezvousIntent", "!matrix-js-sdk/src/indexeddb-worker", "!matrix-js-sdk/src/pushprocessor", "!matrix-js-sdk/src/extensible_events_v1", diff --git a/src/components/views/auth/LoginWithQR-types.ts b/src/components/views/auth/LoginWithQR-types.ts new file mode 100644 index 00000000000..589be54b865 --- /dev/null +++ b/src/components/views/auth/LoginWithQR-types.ts @@ -0,0 +1,64 @@ +/* +Copyright 2024 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. +*/ + +/** + * The intention of this enum is to have a mode that scans a QR code instead of generating one. + */ +export enum Mode { + /** + * A QR code with be generated and shown + */ + Show = "show", + // Scan = "scan", +} + +export enum Phase { + Loading, + ShowingQR, + Connecting, + /** + * @deprecated the MSC3906 implementation is deprecated in favour of MSC4108. + */ + Connected, + OutOfBandConfirmation, + ShowChannelSecure, + WaitingForDevice, + Verifying, + Continue, + Error, +} + +/** + * @deprecated the MSC3906 implementation is deprecated in favour of MSC4108. + */ +export type LegacyPhase = + | Phase.Loading + | Phase.ShowingQR + | Phase.Connecting + | Phase.Connected + | Phase.WaitingForDevice + | Phase.Verifying + | Phase.Error; + +export enum Click { + Cancel, + Decline, + Approve, + TryAgain, + Back, + // ScanQr, + ShowQr, +} diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index 1b1a8688712..f11deb4999c 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -14,67 +14,19 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { lazy, Suspense } from "react"; -// We import "matrix-js-sdk/src/rendezvous" asynchronously to avoid importing the entire Rust Crypto WASM into the main bundle. -import { RendezvousFailureReason } from "matrix-js-sdk/src/rendezvous/RendezvousFailureReason"; -import { RendezvousIntent } from "matrix-js-sdk/src/rendezvous/RendezvousIntent"; +import React from "react"; +import { + RendezvousFailureReason, + RendezvousIntent, + MSC4108SignInWithQR, + MSC4108SecureChannel, + MSC4108RendezvousSession, +} from "matrix-js-sdk/src/rendezvous"; import { logger } from "matrix-js-sdk/src/logger"; import { MatrixClient } from "matrix-js-sdk/src/matrix"; -import type { MSC4108SignInWithQR } from "matrix-js-sdk/src/rendezvous"; -import Spinner from "../elements/Spinner"; - -// We import `LoginWithQRFlow` asynchronously to avoid importing the entire Rust Crypto WASM into the main bundle. -const LoginWithQRFlow = lazy(() => import("./LoginWithQRFlow")); - -/** - * The intention of this enum is to have a mode that scans a QR code instead of generating one. - */ -export enum Mode { - /** - * A QR code with be generated and shown - */ - Show = "show", - // Scan = "scan", -} - -export enum Phase { - Loading, - ShowingQR, - Connecting, - /** - * @deprecated the MSC3906 implementation is deprecated in favour of MSC4108. - */ - Connected, - OutOfBandConfirmation, - ShowChannelSecure, - WaitingForDevice, - Verifying, - Continue, - Error, -} - -/** - * @deprecated the MSC3906 implementation is deprecated in favour of MSC4108. - */ -export type LegacyPhase = - | Phase.Loading - | Phase.ShowingQR - | Phase.Connecting - | Phase.Connected - | Phase.WaitingForDevice - | Phase.Verifying - | Phase.Error; - -export enum Click { - Cancel, - Decline, - Approve, - TryAgain, - Back, - // ScanQr, - ShowQr, -} +import { Mode, Phase, Click } from "./LoginWithQR-types"; +import LoginWithQRFlow from "./LoginWithQRFlow"; interface IProps { client?: MatrixClient; @@ -161,21 +113,19 @@ export default class LoginWithQR extends React.Component { private generateAndShowCode = async (): Promise => { let rendezvous: MSC4108SignInWithQR; try { - const Rendezvous = await import("matrix-js-sdk/src/rendezvous"); - const fallbackRzServer = this.props.client?.getClientWellKnown()?.["io.element.rendezvous"]?.server ?? "https://rendezvous.lab.element.dev"; - const transport = new Rendezvous.MSC4108RendezvousSession({ + const transport = new MSC4108RendezvousSession({ onFailure: this.onFailure, client: this.props.client, fallbackRzServer, }); await transport.send(""); - const channel = new Rendezvous.MSC4108SecureChannel(transport, undefined, this.onFailure); + const channel = new MSC4108SecureChannel(transport, undefined, this.onFailure); - rendezvous = new Rendezvous.MSC4108SignInWithQR(channel, false, this.props.client, this.onFailure); + rendezvous = new MSC4108SignInWithQR(channel, false, this.props.client, this.onFailure); await rendezvous.generateCode(); this.setState({ @@ -297,15 +247,13 @@ export default class LoginWithQR extends React.Component { public render(): React.ReactNode { logger.info("LoginWithQR render"); return ( - }> - - + ); } } diff --git a/src/components/views/auth/LoginWithQRFlow.tsx b/src/components/views/auth/LoginWithQRFlow.tsx index c733b34c098..10b2e96d25a 100644 --- a/src/components/views/auth/LoginWithQRFlow.tsx +++ b/src/components/views/auth/LoginWithQRFlow.tsx @@ -24,7 +24,7 @@ import AccessibleButton from "../elements/AccessibleButton"; import QRCode from "../elements/QRCode"; import Spinner from "../elements/Spinner"; import { Icon as CheckmarkIcon } from "../../../../res/img/element-icons/check.svg"; -import { Click, Phase } from "./LoginWithQR"; +import { Click, Phase } from "./LoginWithQR-types"; import SdkConfig from "../../../SdkConfig"; interface IProps { diff --git a/src/components/views/settings/tabs/user/SessionManagerTab.tsx b/src/components/views/settings/tabs/user/SessionManagerTab.tsx index 3618ca9794a..9676f630831 100644 --- a/src/components/views/settings/tabs/user/SessionManagerTab.tsx +++ b/src/components/views/settings/tabs/user/SessionManagerTab.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; +import React, { lazy, Suspense, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; import { discoverAndValidateOIDCIssuerWellKnown, MatrixClient } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; @@ -32,7 +32,7 @@ import { ExtendedDevice } from "../../devices/types"; import { deleteDevicesWithInteractiveAuth } from "../../devices/deleteDevices"; import SettingsTab from "../SettingsTab"; import LoginWithQRSection from "../../devices/LoginWithQRSection"; -import LoginWithQR, { Mode } from "../../../auth/LoginWithQR"; +import { Mode } from "../../../auth/LoginWithQR-types"; import { useAsyncMemo } from "../../../../../hooks/useAsyncMemo"; import QuestionDialog from "../../../dialogs/QuestionDialog"; import { FilterVariation } from "../../devices/filter"; @@ -40,6 +40,10 @@ import { OtherSessionsSectionHeading } from "../../devices/OtherSessionsSectionH import { SettingsSection } from "../../shared/SettingsSection"; import { OidcLogoutDialog } from "../../../dialogs/oidc/OidcLogoutDialog"; import { SDKContext } from "../../../../../contexts/SDKContext"; +import Spinner from "../../../elements/Spinner"; + +// We import `LoginWithQR` asynchronously to avoid importing the entire Rust Crypto WASM into the main bundle. +const LoginWithQR = lazy(() => import("../../../auth/LoginWithQR")); const confirmSignOut = async (sessionsToSignOutCount: number): Promise => { const { finished } = Modal.createDialog(QuestionDialog, { @@ -280,7 +284,11 @@ const SessionManagerTab: React.FC = () => { }, [setSignInWithQrMode]); if (signInWithQrMode) { - return ; + return ( + }> + + + ); } return ( diff --git a/test/components/views/settings/devices/LoginWithQR-test.tsx b/test/components/views/settings/devices/LoginWithQR-test.tsx index cdbb46a8b68..7a1a269064e 100644 --- a/test/components/views/settings/devices/LoginWithQR-test.tsx +++ b/test/components/views/settings/devices/LoginWithQR-test.tsx @@ -20,7 +20,8 @@ import React from "react"; import { MSC3906Rendezvous, RendezvousFailureReason } from "matrix-js-sdk/src/rendezvous"; import { HTTPError, LoginTokenPostResponse } from "matrix-js-sdk/src/matrix"; -import LoginWithQR, { Click, Mode, Phase } from "../../../../../src/components/views/auth/LoginWithQR"; +import LoginWithQR from "../../../../../src/components/views/auth/LoginWithQR"; +import { Click, Mode, Phase } from "../../../../../src/components/views/auth/LoginWithQR-types"; import type { MatrixClient } from "matrix-js-sdk/src/matrix"; jest.mock("matrix-js-sdk/src/rendezvous"); diff --git a/test/components/views/settings/devices/LoginWithQRFlow-test.tsx b/test/components/views/settings/devices/LoginWithQRFlow-test.tsx index 614a8d3ffd4..21863c84a49 100644 --- a/test/components/views/settings/devices/LoginWithQRFlow-test.tsx +++ b/test/components/views/settings/devices/LoginWithQRFlow-test.tsx @@ -19,12 +19,8 @@ import React from "react"; import { RendezvousFailureReason } from "matrix-js-sdk/src/rendezvous"; import LoginWithQRFlow from "../../../../../src/components/views/auth/LoginWithQRFlow"; -import { - Click, - Phase, - LoginWithQRFailureReason, - FailureReason, -} from "../../../../../src/components/views/auth/LoginWithQR"; +import { LoginWithQRFailureReason, FailureReason } from "../../../../../src/components/views/auth/LoginWithQR"; +import { Click, Phase } from "../../../../../src/components/views/auth/LoginWithQR-types"; describe("", () => { const onClick = jest.fn(); From f808b49065b6b43e89fcbe56c2299600b963cd8d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 26 Mar 2024 09:54:03 +0000 Subject: [PATCH 17/98] Restore legacy QR code login Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/auth/Login.tsx | 2 - .../views/auth/LoginWithQR-types.ts | 15 +- src/components/views/auth/LoginWithQR.tsx | 132 +++++++++++++++--- src/components/views/auth/LoginWithQRFlow.tsx | 118 ++++++++-------- src/i18n/strings/en_EN.json | 10 ++ .../settings/devices/LoginWithQR-test.tsx | 21 +-- .../settings/devices/LoginWithQRFlow-test.tsx | 2 +- .../LoginWithQRFlow-test.tsx.snap | 42 ++++++ 8 files changed, 235 insertions(+), 107 deletions(-) diff --git a/src/components/structures/auth/Login.tsx b/src/components/structures/auth/Login.tsx index 11b843d2218..5fadde7cbea 100644 --- a/src/components/structures/auth/Login.tsx +++ b/src/components/structures/auth/Login.tsx @@ -89,7 +89,6 @@ interface IState { serverIsAlive: boolean; serverErrorIsFatal: boolean; serverDeadError?: ReactNode; - loginWithQrInProgress: boolean; } type OnPasswordLogin = { @@ -126,7 +125,6 @@ export default class LoginComponent extends React.PureComponent serverIsAlive: true, serverErrorIsFatal: false, serverDeadError: "", - loginWithQrInProgress: false, }; // map from login step type to a function which will render a control diff --git a/src/components/views/auth/LoginWithQR-types.ts b/src/components/views/auth/LoginWithQR-types.ts index 589be54b865..6af2e622363 100644 --- a/src/components/views/auth/LoginWithQR-types.ts +++ b/src/components/views/auth/LoginWithQR-types.ts @@ -22,23 +22,21 @@ export enum Mode { * A QR code with be generated and shown */ Show = "show", - // Scan = "scan", } export enum Phase { Loading, ShowingQR, Connecting, - /** - * @deprecated the MSC3906 implementation is deprecated in favour of MSC4108. - */ - Connected, + // The following are specific to MSC4108 OutOfBandConfirmation, - ShowChannelSecure, WaitingForDevice, Verifying, - Continue, Error, + /** + * @deprecated the MSC3906 implementation is deprecated in favour of MSC4108. + */ + LegacyConnected, } /** @@ -48,7 +46,7 @@ export type LegacyPhase = | Phase.Loading | Phase.ShowingQR | Phase.Connecting - | Phase.Connected + | Phase.LegacyConnected | Phase.WaitingForDevice | Phase.Verifying | Phase.Error; @@ -59,6 +57,5 @@ export enum Click { Approve, TryAgain, Back, - // ScanQr, ShowQr, } diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index f11deb4999c..ac9624cb709 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -21,26 +21,38 @@ import { MSC4108SignInWithQR, MSC4108SecureChannel, MSC4108RendezvousSession, + MSC3906Rendezvous, + MSC3886SimpleHttpRendezvousTransport, + MSC3903ECDHv2RendezvousChannel, + MSC3903ECDHPayload, } from "matrix-js-sdk/src/rendezvous"; import { logger } from "matrix-js-sdk/src/logger"; -import { MatrixClient } from "matrix-js-sdk/src/matrix"; +import { HTTPError, MatrixClient } from "matrix-js-sdk/src/matrix"; import { Mode, Phase, Click } from "./LoginWithQR-types"; import LoginWithQRFlow from "./LoginWithQRFlow"; +import { wrapRequestWithDialog } from "../../../utils/UserInteractiveAuth"; +import { _t } from "../../../languageHandler"; interface IProps { client?: MatrixClient; mode: Mode; + legacy: boolean; onFinished(...args: any): void; } interface IState { phase: Phase; - rendezvous?: MSC4108SignInWithQR; + rendezvous?: MSC3906Rendezvous | MSC4108SignInWithQR; + mediaPermissionError?: boolean; + + // MSC3906 + confirmationDigits?: string; + + // MSC4108 verificationUri?: string; userCode?: string; - failureReason?: RendezvousFailureReason; - mediaPermissionError?: boolean; + failureReason?: FailureReason; lastScannedCode?: Buffer; ourIntent: RendezvousIntent; homeserverBaseUrl?: string; @@ -88,12 +100,13 @@ export default class LoginWithQR extends React.Component { } private async updateMode(mode: Mode): Promise { - logger.info(`updateMode: ${mode}`); this.setState({ phase: Phase.Loading }); if (this.state.rendezvous) { const rendezvous = this.state.rendezvous; rendezvous.onFailure = undefined; - // await rendezvous.cancel(RendezvousFailureReason.UserCancelled); + if (this.props.legacy) { + await rendezvous.cancel(RendezvousFailureReason.UserCancelled); + } this.setState({ rendezvous: undefined }); } if (mode === Mode.Show) { @@ -110,22 +123,78 @@ export default class LoginWithQR extends React.Component { } } - private generateAndShowCode = async (): Promise => { - let rendezvous: MSC4108SignInWithQR; + private async legacyApproveLogin(): Promise { + if (!(this.state.rendezvous instanceof MSC3906Rendezvous)) { + throw new Error("Rendezvous not found"); + } + if (!this.props.client) { + throw new Error("No client to approve login with"); + } + this.setState({ phase: Phase.Loading }); + try { - const fallbackRzServer = - this.props.client?.getClientWellKnown()?.["io.element.rendezvous"]?.server ?? - "https://rendezvous.lab.element.dev"; - const transport = new MSC4108RendezvousSession({ - onFailure: this.onFailure, - client: this.props.client, - fallbackRzServer, - }); - await transport.send(""); + logger.info("Requesting login token"); + + const { login_token: loginToken } = await wrapRequestWithDialog(this.props.client.requestLoginToken, { + matrixClient: this.props.client, + title: _t("auth|qr_code_login|sign_in_new_device"), + })(); + + this.setState({ phase: Phase.WaitingForDevice }); + + const newDeviceId = await this.state.rendezvous.approveLoginOnExistingDevice(loginToken); + if (!newDeviceId) { + // user denied + return; + } + if (!this.props.client.getCrypto()) { + // no E2EE to set up + this.props.onFinished(true); + return; + } + this.setState({ phase: Phase.Verifying }); + await this.state.rendezvous.verifyNewDeviceOnExistingDevice(); + // clean up our state: + try { + await this.state.rendezvous.close(); + } finally { + this.setState({ rendezvous: undefined }); + } + this.props.onFinished(true); + } catch (e) { + logger.error("Error whilst approving sign in", e); + if (e instanceof HTTPError && e.httpStatus === 429) { + // 429: rate limit + this.setState({ phase: Phase.Error, failureReason: LoginWithQRFailureReason.RateLimited }); + return; + } + this.setState({ phase: Phase.Error, failureReason: RendezvousFailureReason.Unknown }); + } + } - const channel = new MSC4108SecureChannel(transport, undefined, this.onFailure); + private generateAndShowCode = async (): Promise => { + let rendezvous: MSC4108SignInWithQR | MSC3906Rendezvous; + try { + const fallbackRzServer = this.props.client?.getClientWellKnown()?.["io.element.rendezvous"]?.server; - rendezvous = new MSC4108SignInWithQR(channel, false, this.props.client, this.onFailure); + if (this.props.legacy) { + const transport = new MSC3886SimpleHttpRendezvousTransport({ + onFailure: this.onFailure, + client: this.props.client!, + fallbackRzServer, + }); + const channel = new MSC3903ECDHv2RendezvousChannel(transport, undefined, this.onFailure); + rendezvous = new MSC3906Rendezvous(channel, this.props.client!, this.onFailure); + } else { + const transport = new MSC4108RendezvousSession({ + onFailure: this.onFailure, + client: this.props.client, + fallbackRzServer, + }); + await transport.send(""); + const channel = new MSC4108SecureChannel(transport, undefined, this.onFailure); + rendezvous = new MSC4108SignInWithQR(channel, false, this.props.client, this.onFailure); + } await rendezvous.generateCode(); this.setState({ @@ -140,7 +209,10 @@ export default class LoginWithQR extends React.Component { } try { - if (this.state.ourIntent === RendezvousIntent.LOGIN_ON_NEW_DEVICE) { + if (rendezvous instanceof MSC3906Rendezvous) { + const confirmationDigits = await rendezvous.startAfterShowingCode(); + this.setState({ phase: Phase.LegacyConnected, confirmationDigits }); + } else if (this.state.ourIntent === RendezvousIntent.LOGIN_ON_NEW_DEVICE) { // MSC4108-Flow: ExistingScanned // we get the homserver URL from the secure channel, but we don't trust it yet @@ -174,7 +246,7 @@ export default class LoginWithQR extends React.Component { }; private approveLoginAfterShowingCode = async (): Promise => { - if (!this.state.rendezvous) { + if (!(this.state.rendezvous instanceof MSC4108SignInWithQR)) { throw new Error("Rendezvous not found"); } @@ -206,6 +278,7 @@ export default class LoginWithQR extends React.Component { public reset(): void { this.setState({ rendezvous: undefined, + confirmationDigits: undefined, verificationUri: undefined, failureReason: undefined, userCode: undefined, @@ -223,7 +296,7 @@ export default class LoginWithQR extends React.Component { this.props.onFinished(false); break; case Click.Approve: - await this.approveLoginAfterShowingCode(); + await (this.props.legacy ? this.legacyApproveLogin() : this.approveLoginAfterShowingCode()); break; case Click.Decline: await this.state.rendezvous?.declineLoginOnExistingDevice(); @@ -245,7 +318,20 @@ export default class LoginWithQR extends React.Component { }; public render(): React.ReactNode { - logger.info("LoginWithQR render"); + if (this.state.rendezvous instanceof MSC3906Rendezvous) { + return ( + + ); + } + return ( { + code?: string; + confirmationDigits?: string; +} + +interface Props { phase: Phase; code?: Uint8Array; onClick(type: Click): Promise; - failureReason?: RendezvousFailureReason; + failureReason?: FailureReason; userCode?: string; } /** * A component that implements the UI for sign in and E2EE set up with a QR code. * - * This uses the unstable feature of MSC3906: https://github.com/matrix-org/matrix-spec-proposals/pull/3906 + * This supports the unstable features of MSC3906 and MSC4108 */ -export default class LoginWithQRFlow extends React.Component { - public constructor(props: IProps) { +export default class LoginWithQRFlow extends React.Component> { + public constructor(props: XOR) { super(props); } @@ -70,7 +80,6 @@ export default class LoginWithQRFlow extends React.Component { }; public render(): React.ReactNode { - logger.info(`LoginWithQRFlow render: phase=${this.props.phase}`); let main: JSX.Element | undefined; let buttons: JSX.Element | undefined; let backButton = true; @@ -101,6 +110,9 @@ export default class LoginWithQRFlow extends React.Component { case RendezvousFailureReason.UserCancelled: cancellationMessage = _t("auth|qr_code_login|error_request_cancelled"); break; + case LoginWithQRFailureReason.RateLimited: + cancellationMessage = _t("auth|qr_code_login|error_rate_limited"); + break; case RendezvousFailureReason.Unknown: cancellationMessage = _t("auth|qr_code_login|error_unexpected"); break; @@ -127,86 +139,46 @@ export default class LoginWithQRFlow extends React.Component { ); break; - case Phase.OutOfBandConfirmation: + case Phase.LegacyConnected: backButton = false; main = ( <> -

- Confirm that you see a green checkmark on both devices to to verify that the connection is - secure. -

+

{_t("auth|qr_code_login|confirm_code_match")}

+
{this.props.confirmationDigits}
- +
+
{_t("auth|qr_code_login|approve_access_warning")}
- {this.props.userCode ? ( -
-

Security code

-

If asked, enter the code below on your other device.

-

{this.props.userCode}

-
- ) : null} ); buttons = ( <> - - Yes, I do - - No, I don't + {_t("action|cancel")} - - ); - break; - case Phase.Continue: - // PROTOTYPE: I don't think we would offer the ability to scan from a web browser, so this is really just for the prototype. - // title = "Go to your account to continue"; - // titleIcon = ; - backButton = false; - main = ( - <> -

Open your servername.io account to link your new device.

-

- This screen is only needed due to Web Browser UX restrictions. If this was a native mobile - app like Element X then the OIDC Provider consent screen could be opened automatically. - Also, we don't plan to offer the ability to scan from a web browser so this is a non-issue. -

-
-
- -
-
- - ); - - buttons = ( - <> - Continue + {_t("action|approve")} ); break; - case Phase.ShowChannelSecure: + case Phase.OutOfBandConfirmation: backButton = false; main = ( <> -

You’ll be asked to confirm that you can see a green checkmark on this device..

+

{_t("auth|qr_code_login|confirm_green_checkmark_title")}

+

{_t("auth|qr_code_login|confirm_green_checkmark")}

@@ -214,19 +186,41 @@ export default class LoginWithQRFlow extends React.Component {
{this.props.userCode ? (
-

Security code

-

If asked, enter the code below on your other device.

+

{_t("auth|qr_code_login|security_code")}

+

{_t("auth|qr_code_login|security_code_prompt")}

{this.props.userCode}

) : null} ); + + buttons = ( + <> + + {_t("auth|qr_code_login|reject_action")} + + + {_t("auth|qr_code_login|confirm_action")} + + + ); break; case Phase.ShowingQR: if (this.props.code) { + const data = + typeof this.props.code !== "string" ? this.props.code : Buffer.from(this.props.code ?? ""); + const code = (
- +
); main = ( @@ -267,8 +261,8 @@ export default class LoginWithQRFlow extends React.Component { {this.simpleSpinner(_t("auth|qr_code_login|waiting_for_device"))} {this.props.userCode ? (
-

Security code

-

If asked, enter the code below on your other device.

+

{_t("auth|qr_code_login|security_code")}

+

{_t("auth|qr_code_login|security_code_prompt")}

{this.props.userCode}

) : null} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 96913469bf5..9f19c2db6ca 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -245,7 +245,15 @@ "phone_label": "Phone", "phone_optional_label": "Phone (optional)", "qr_code_login": { + "approve_access_warning": "By approving access for this device, it will have full access to your account.", "completing_setup": "Completing set up of your new device", + "confirm_code_match": "Check that the code below matches with your other device:", + "confirm_green_checkmark_title": "Do you see a green checkmark on your other device?", + "confirm_green_checkmark": "Confirm that you see a green checkmark on both devices to to verify that the connection is secure.", + "security_code": "Security code", + "security_code_prompt": "If asked, enter the code below on your other device.", + "confirm_action": "Yes, I do", + "reject_action": "No, I don't", "connecting": "Connecting…", "error_device_already_signed_in": "The other device is already signed in.", "error_device_not_signed_in": "The other device isn't signed in.", @@ -253,6 +261,7 @@ "error_homeserver_lacks_support": "The homeserver doesn't support signing in another device.", "error_invalid_scanned_code": "The scanned code is invalid.", "error_linking_incomplete": "The linking wasn't completed in the required time.", + "error_rate_limited": "Too many attempts in a short time. Wait some time before trying again.", "error_request_cancelled": "The request was cancelled.", "error_request_declined": "The request was declined on the other device.", "error_unexpected": "An unexpected error occurred.", @@ -262,6 +271,7 @@ "scan_code_instruction": "Scan the QR code with another device", "scan_qr_code": "Scan QR code", "select_qr_code": "Select \"%(scanQRCode)s\"", + "sign_in_new_device": "Sign in new device", "waiting_for_device": "Waiting for device to sign in" }, "register_action": "Create Account", diff --git a/test/components/views/settings/devices/LoginWithQR-test.tsx b/test/components/views/settings/devices/LoginWithQR-test.tsx index 7a1a269064e..1ec19da6eab 100644 --- a/test/components/views/settings/devices/LoginWithQR-test.tsx +++ b/test/components/views/settings/devices/LoginWithQR-test.tsx @@ -65,6 +65,7 @@ function unresolvedPromise(): Promise { describe("", () => { let client!: MockedObject; const defaultProps = { + legacy: true, mode: Mode.Show, onFinished: jest.fn(), }; @@ -216,12 +217,12 @@ describe("", () => { await waitFor(() => expect(mockedFlow).toHaveBeenLastCalledWith( expect.objectContaining({ - phase: Phase.Connected, + phase: Phase.LegacyConnected, }), ), ); expect(mockedFlow).toHaveBeenLastCalledWith({ - phase: Phase.Connected, + phase: Phase.LegacyConnected, confirmationDigits: mockConfirmationDigits, onClick: expect.any(Function), }); @@ -246,12 +247,12 @@ describe("", () => { await waitFor(() => expect(mockedFlow).toHaveBeenLastCalledWith( expect.objectContaining({ - phase: Phase.Connected, + phase: Phase.LegacyConnected, }), ), ); expect(mockedFlow).toHaveBeenLastCalledWith({ - phase: Phase.Connected, + phase: Phase.LegacyConnected, confirmationDigits: mockConfirmationDigits, onClick: expect.any(Function), }); @@ -286,12 +287,12 @@ describe("", () => { await waitFor(() => expect(mockedFlow).toHaveBeenLastCalledWith( expect.objectContaining({ - phase: Phase.Connected, + phase: Phase.LegacyConnected, }), ), ); expect(mockedFlow).toHaveBeenLastCalledWith({ - phase: Phase.Connected, + phase: Phase.LegacyConnected, confirmationDigits: mockConfirmationDigits, onClick: expect.any(Function), }); @@ -323,12 +324,12 @@ describe("", () => { await waitFor(() => expect(mockedFlow).toHaveBeenLastCalledWith( expect.objectContaining({ - phase: Phase.Connected, + phase: Phase.LegacyConnected, }), ), ); expect(mockedFlow).toHaveBeenLastCalledWith({ - phase: Phase.Connected, + phase: Phase.LegacyConnected, confirmationDigits: mockConfirmationDigits, onClick: expect.any(Function), }); @@ -353,12 +354,12 @@ describe("", () => { await waitFor(() => expect(mockedFlow).toHaveBeenLastCalledWith( expect.objectContaining({ - phase: Phase.Connected, + phase: Phase.LegacyConnected, }), ), ); expect(mockedFlow).toHaveBeenLastCalledWith({ - phase: Phase.Connected, + phase: Phase.LegacyConnected, confirmationDigits: mockConfirmationDigits, onClick: expect.any(Function), }); diff --git a/test/components/views/settings/devices/LoginWithQRFlow-test.tsx b/test/components/views/settings/devices/LoginWithQRFlow-test.tsx index 21863c84a49..702dc447af0 100644 --- a/test/components/views/settings/devices/LoginWithQRFlow-test.tsx +++ b/test/components/views/settings/devices/LoginWithQRFlow-test.tsx @@ -73,7 +73,7 @@ describe("", () => { }); it("renders code when connected", async () => { - const { container } = render(getComponent({ phase: Phase.Connected, confirmationDigits: "mock-digits" })); + const { container } = render(getComponent({ phase: Phase.LegacyConnected, confirmationDigits: "mock-digits" })); expect(screen.getAllByText("mock-digits")).toHaveLength(1); expect(screen.getAllByTestId("decline-login-button")).toHaveLength(1); expect(screen.getAllByTestId("approve-login-button")).toHaveLength(1); diff --git a/test/components/views/settings/devices/__snapshots__/LoginWithQRFlow-test.tsx.snap b/test/components/views/settings/devices/__snapshots__/LoginWithQRFlow-test.tsx.snap index 953e28635e8..1f6c51d99e1 100644 --- a/test/components/views/settings/devices/__snapshots__/LoginWithQRFlow-test.tsx.snap +++ b/test/components/views/settings/devices/__snapshots__/LoginWithQRFlow-test.tsx.snap @@ -294,6 +294,48 @@ exports[` errors renders rate_limited 1`] = `
`; +exports[` errors renders unexpected_message 1`] = ` +
+
+
+
+

+ The request was cancelled. +

+
+
+
+ Try again +
+
+ Cancel +
+
+
+
+`; + exports[` errors renders unknown 1`] = `
Date: Tue, 26 Mar 2024 10:42:23 +0000 Subject: [PATCH 18/98] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/auth/LoginWithQR-types.ts | 12 ------------ .../views/settings/devices/LoginWithQRSection.tsx | 10 ++++++---- .../views/settings/tabs/user/SessionManagerTab.tsx | 7 ++++++- 3 files changed, 12 insertions(+), 17 deletions(-) diff --git a/src/components/views/auth/LoginWithQR-types.ts b/src/components/views/auth/LoginWithQR-types.ts index 6af2e622363..6ed9cc1cf8d 100644 --- a/src/components/views/auth/LoginWithQR-types.ts +++ b/src/components/views/auth/LoginWithQR-types.ts @@ -39,18 +39,6 @@ export enum Phase { LegacyConnected, } -/** - * @deprecated the MSC3906 implementation is deprecated in favour of MSC4108. - */ -export type LegacyPhase = - | Phase.Loading - | Phase.ShowingQR - | Phase.Connecting - | Phase.LegacyConnected - | Phase.WaitingForDevice - | Phase.Verifying - | Phase.Error; - export enum Click { Cancel, Decline, diff --git a/src/components/views/settings/devices/LoginWithQRSection.tsx b/src/components/views/settings/devices/LoginWithQRSection.tsx index eca87887bc6..9eb132093b2 100644 --- a/src/components/views/settings/devices/LoginWithQRSection.tsx +++ b/src/components/views/settings/devices/LoginWithQRSection.tsx @@ -54,6 +54,9 @@ export default class LoginWithQRSection extends React.Component { const msc3886Supported = !!this.props.versions?.unstable_features?.["org.matrix.msc3886"] || !!this.props.wellKnown?.["io.element.rendezvous"]?.server; + const msc4108Supported = + !!this.props.versions?.unstable_features?.["org.matrix.msc4108"] || + !!this.props.wellKnown?.["io.element.rendezvous"]?.server; const deviceAuthorizationGrantSupported = this.props.oidcClientConfig && @@ -62,12 +65,11 @@ export default class LoginWithQRSection extends React.Component { "urn:ietf:params:oauth:grant-type:device_code", ); - logger.info( + logger.debug( `getLoginTokenSupported: ${getLoginTokenSupported} msc3886Supported: ${msc3886Supported} deviceAuthorizationGrantSupported: ${deviceAuthorizationGrantSupported}`, ); - // PROTOTYPE: we hard code this to always show: - // We aren't checking for MSC4108 support - const offerShowQr = true || ((getLoginTokenSupported || deviceAuthorizationGrantSupported) && msc3886Supported); + const offerShowQr = + (getLoginTokenSupported || deviceAuthorizationGrantSupported) && (msc3886Supported || msc4108Supported); // don't show anything if no method is available if (!offerShowQr) { diff --git a/src/components/views/settings/tabs/user/SessionManagerTab.tsx b/src/components/views/settings/tabs/user/SessionManagerTab.tsx index 9676f630831..bb8467ecfa3 100644 --- a/src/components/views/settings/tabs/user/SessionManagerTab.tsx +++ b/src/components/views/settings/tabs/user/SessionManagerTab.tsx @@ -286,7 +286,12 @@ const SessionManagerTab: React.FC = () => { if (signInWithQrMode) { return ( }> - + ); } From 31ac294e7d38b4b2bd7a9200e5292e1358ebe773 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 26 Mar 2024 14:26:34 +0000 Subject: [PATCH 19/98] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../views/settings/tabs/user/SessionManagerTab-test.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx b/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx index 60b6bade4da..0bde72bbcf0 100644 --- a/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx +++ b/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx @@ -182,6 +182,7 @@ describe("", () => { getPushers: jest.fn(), setPusher: jest.fn(), setLocalNotificationSettings: jest.fn(), + getAuthIssuer: jest.fn().mockReturnValue(new Promise(() => {})), }); jest.clearAllMocks(); jest.spyOn(logger, "error").mockRestore(); @@ -1539,13 +1540,13 @@ describe("", () => { }); it("enters qr code login section when show QR code button clicked", async () => { - const { getByText, getByTestId } = render(getComponent()); + const { getByText, findByTestId } = render(getComponent()); // wait for versions call to settle await flushPromises(); fireEvent.click(getByText("Show QR code")); - expect(getByTestId("login-with-qr")).toBeTruthy(); + await expect(findByTestId("login-with-qr")).resolves.toBeTruthy(); }); }); }); From e01610eb81da26b75d7bcaade2f807cc9be14ba7 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 26 Mar 2024 14:32:00 +0000 Subject: [PATCH 20/98] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../settings/devices/LoginWithQRSection.tsx | 23 +++++----- .../tabs/user/SessionManagerTab-test.tsx | 42 ++++++++++++++++++- 2 files changed, 53 insertions(+), 12 deletions(-) diff --git a/src/components/views/settings/devices/LoginWithQRSection.tsx b/src/components/views/settings/devices/LoginWithQRSection.tsx index 9eb132093b2..3932f4f9c74 100644 --- a/src/components/views/settings/devices/LoginWithQRSection.tsx +++ b/src/components/views/settings/devices/LoginWithQRSection.tsx @@ -58,18 +58,19 @@ export default class LoginWithQRSection extends React.Component { !!this.props.versions?.unstable_features?.["org.matrix.msc4108"] || !!this.props.wellKnown?.["io.element.rendezvous"]?.server; - const deviceAuthorizationGrantSupported = - this.props.oidcClientConfig && - "metadata" in this.props.oidcClientConfig && - this.props.oidcClientConfig.metadata.grant_types_supported.includes( - "urn:ietf:params:oauth:grant-type:device_code", - ); - - logger.debug( - `getLoginTokenSupported: ${getLoginTokenSupported} msc3886Supported: ${msc3886Supported} deviceAuthorizationGrantSupported: ${deviceAuthorizationGrantSupported}`, + const deviceAuthorizationGrantSupported = this.props.oidcClientConfig?.metadata?.grant_types_supported.includes( + "urn:ietf:params:oauth:grant-type:device_code", ); - const offerShowQr = - (getLoginTokenSupported || deviceAuthorizationGrantSupported) && (msc3886Supported || msc4108Supported); + + logger.debug({ + msc3886Supported, + getLoginTokenSupported, + msc4108Supported, + deviceAuthorizationGrantSupported, + }); + const offerShowQr = this.props.oidcClientConfig + ? deviceAuthorizationGrantSupported && msc4108Supported + : getLoginTokenSupported && msc3886Supported; // don't show anything if no method is available if (!offerShowQr) { diff --git a/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx b/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx index 0bde72bbcf0..f2336716811 100644 --- a/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx +++ b/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx @@ -1510,7 +1510,7 @@ describe("", () => { expect(checkbox.getAttribute("aria-checked")).toEqual("false"); }); - describe("QR code login", () => { + describe("MSC3906 QR code login", () => { const settingsValueSpy = jest.spyOn(SettingsStore, "getValue"); beforeEach(() => { @@ -1549,4 +1549,44 @@ describe("", () => { await expect(findByTestId("login-with-qr")).resolves.toBeTruthy(); }); }); + + describe("MSC4108 QR code login", () => { + const settingsValueSpy = jest.spyOn(SettingsStore, "getValue"); + + beforeEach(() => { + settingsValueSpy.mockClear().mockReturnValue(false); + // enable server support for qr login + mockClient.getVersions.mockResolvedValue({ + versions: [], + unstable_features: { + "org.matrix.msc4108": true, + }, + }); + mockClient.getCapabilities.mockResolvedValue({ + [GET_LOGIN_TOKEN_CAPABILITY.name]: { + enabled: true, + }, + }); + }); + + it("renders qr code login section", async () => { + const { getByText } = render(getComponent()); + + // wait for versions call to settle + await flushPromises(); + + expect(getByText("Link new device")).toBeTruthy(); + expect(getByText("Show QR code")).toBeTruthy(); + }); + + it("enters qr code login section when show QR code button clicked", async () => { + const { getByText, findByTestId } = render(getComponent()); + // wait for versions call to settle + await flushPromises(); + + fireEvent.click(getByText("Show QR code")); + + await expect(findByTestId("login-with-qr")).resolves.toBeTruthy(); + }); + }); }); From 97c1fe882c1ce0298571c3a32cdfba0ed5d4f6cd Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 26 Mar 2024 14:32:29 +0000 Subject: [PATCH 21/98] i18n Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/i18n/strings/en_EN.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 9f19c2db6ca..033b21817b5 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -247,13 +247,10 @@ "qr_code_login": { "approve_access_warning": "By approving access for this device, it will have full access to your account.", "completing_setup": "Completing set up of your new device", + "confirm_action": "Yes, I do", "confirm_code_match": "Check that the code below matches with your other device:", - "confirm_green_checkmark_title": "Do you see a green checkmark on your other device?", "confirm_green_checkmark": "Confirm that you see a green checkmark on both devices to to verify that the connection is secure.", - "security_code": "Security code", - "security_code_prompt": "If asked, enter the code below on your other device.", - "confirm_action": "Yes, I do", - "reject_action": "No, I don't", + "confirm_green_checkmark_title": "Do you see a green checkmark on your other device?", "connecting": "Connecting…", "error_device_already_signed_in": "The other device is already signed in.", "error_device_not_signed_in": "The other device isn't signed in.", @@ -268,8 +265,11 @@ "follow_remaining_instructions": "Follow the remaining instructions to verify your other device", "open_element_other_device": "Open %(brand)s on your other device", "point_the_camera": "Point the camera at the QR code shown here", + "reject_action": "No, I don't", "scan_code_instruction": "Scan the QR code with another device", "scan_qr_code": "Scan QR code", + "security_code": "Security code", + "security_code_prompt": "If asked, enter the code below on your other device.", "select_qr_code": "Select \"%(scanQRCode)s\"", "sign_in_new_device": "Sign in new device", "waiting_for_device": "Waiting for device to sign in" From 0d341af4fd5c8c036e1aa69c4888f9ee0bd80385 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 26 Mar 2024 15:24:13 +0000 Subject: [PATCH 22/98] Add test Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../tabs/user/SessionManagerTab-test.tsx | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx b/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx index f2336716811..4b3ef61312e 100644 --- a/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx +++ b/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx @@ -36,6 +36,7 @@ import { } from "matrix-js-sdk/src/matrix"; import { mocked, MockedObject } from "jest-mock"; import { TooltipProvider } from "@vector-im/compound-web"; +import fetchMock from "fetch-mock-jest"; import { clearAllModals, @@ -55,6 +56,7 @@ import SettingsStore from "../../../../../../src/settings/SettingsStore"; import { getClientInformationEventType } from "../../../../../../src/utils/device/clientInformation"; import { SDKContext, SdkContextClass } from "../../../../../../src/contexts/SDKContext"; import { OidcClientStore } from "../../../../../../src/stores/oidc/OidcClientStore"; +import { mockOpenIdConfiguration } from "../../../../../test-utils/oidc"; mockPlatformPeg(); @@ -1552,6 +1554,8 @@ describe("", () => { describe("MSC4108 QR code login", () => { const settingsValueSpy = jest.spyOn(SettingsStore, "getValue"); + const issuer = "https://issuer.org"; + const openIdConfiguration = mockOpenIdConfiguration(issuer); beforeEach(() => { settingsValueSpy.mockClear().mockReturnValue(false); @@ -1567,6 +1571,21 @@ describe("", () => { enabled: true, }, }); + mockClient.getAuthIssuer.mockResolvedValue({ issuer }); + fetchMock.mock(`${issuer}/.well-known/openid-configuration`, { + ...openIdConfiguration, + grant_types_supported: [ + ...openIdConfiguration.grant_types_supported, + "urn:ietf:params:oauth:grant-type:device_code", + ], + }); + fetchMock.mock(openIdConfiguration.jwks_uri!, { + status: 200, + headers: { + "Content-Type": "application/json", + }, + keys: [], + }); }); it("renders qr code login section", async () => { From c2248e73ed98201d29032de27035dcc9e95ab082 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Tue, 26 Mar 2024 18:48:09 +0000 Subject: [PATCH 23/98] Prototype of requiring CheckCode Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/auth/LoginWithQR.tsx | 19 ++++++- src/components/views/auth/LoginWithQRFlow.tsx | 57 +++++++++++++++++-- 2 files changed, 68 insertions(+), 8 deletions(-) diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index ac9624cb709..2fb1b227b9c 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -245,11 +245,21 @@ export default class LoginWithQR extends React.Component { } }; - private approveLoginAfterShowingCode = async (): Promise => { + private approveLoginAfterShowingCode = async (checkCode: string | undefined): Promise => { if (!(this.state.rendezvous instanceof MSC4108SignInWithQR)) { throw new Error("Rendezvous not found"); } + if (!checkCode) { + throw new Error("No check code"); + } + + if (this.state.rendezvous?.checkCode !== checkCode) { + // PROTOTYPE: this doesn't actually show in the UI sensibly + // should we prompt to re-enter? + throw new Error("Check code mismatch"); + } + if (this.state.ourIntent === RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE) { // MSC4108-Flow: NewScanned this.setState({ phase: Phase.Loading }); @@ -288,7 +298,7 @@ export default class LoginWithQR extends React.Component { }); } - private onClick = async (type: Click): Promise => { + private onClick = async (type: Click, checkCode?: string): Promise => { switch (type) { case Click.Cancel: await this.state.rendezvous?.cancel(RendezvousFailureReason.UserCancelled); @@ -296,7 +306,7 @@ export default class LoginWithQR extends React.Component { this.props.onFinished(false); break; case Click.Approve: - await (this.props.legacy ? this.legacyApproveLogin() : this.approveLoginAfterShowingCode()); + await (this.props.legacy ? this.legacyApproveLogin() : this.approveLoginAfterShowingCode(checkCode)); break; case Click.Decline: await this.state.rendezvous?.declineLoginOnExistingDevice(); @@ -339,6 +349,9 @@ export default class LoginWithQR extends React.Component { code={this.state.phase === Phase.ShowingQR ? this.state.rendezvous?.code : undefined} failureReason={this.state.phase === Phase.Error ? this.state.failureReason : undefined} userCode={this.state.userCode} + checkCode={ + this.state.phase === Phase.ShowChannelSecure ? this.state.rendezvous?.checkCode : undefined + } /> ); } diff --git a/src/components/views/auth/LoginWithQRFlow.tsx b/src/components/views/auth/LoginWithQRFlow.tsx index c05c96989a4..9ad3ac4c910 100644 --- a/src/components/views/auth/LoginWithQRFlow.tsx +++ b/src/components/views/auth/LoginWithQRFlow.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { RefObject, createRef } from "react"; import { RendezvousFailureReason } from "matrix-js-sdk/src/rendezvous"; import { Icon as ChevronLeftIcon } from "@vector-im/compound-design-tokens/icons/chevron-left.svg"; @@ -40,9 +40,10 @@ interface MSC3906Props extends Pick; + onClick(type: Click, checkCodeEntered?: string): Promise; failureReason?: FailureReason; userCode?: string; + checkCode?: string; } /** @@ -51,6 +52,8 @@ interface Props { * This supports the unstable features of MSC3906 and MSC4108 */ export default class LoginWithQRFlow extends React.Component> { + private checkCodeInput = createRef(null); + public constructor(props: XOR) { super(props); } @@ -58,7 +61,7 @@ export default class LoginWithQRFlow extends React.Component Promise) => { return async (e: React.FormEvent): Promise => { e.preventDefault(); - await this.props.onClick(type); + await this.props.onClick(type, type === Click.Approve ? this.checkCodeInput.current?.value : undefined); }; }; @@ -177,8 +180,52 @@ export default class LoginWithQRFlow extends React.Component -

{_t("auth|qr_code_login|confirm_green_checkmark_title")}

-

{_t("auth|qr_code_login|confirm_green_checkmark")}

+

+ To verify that the connection is secure, please enter the code shown on your other device: +

+

+ +

+
+
+ +
+
+ {this.props.userCode ? ( +
+

Security code

+

If asked, enter the code below on your other device.

+

{this.props.userCode}

+
+ ) : null} + + ); + + buttons = ( + <> + + Continue + + + No code shown + + + ); + break; + case Phase.ShowChannelSecure: + backButton = false; + main = ( + <> +

You’ll be asked to enter the following code on your other device:

+

{this.props.checkCode}

From ee5fcedd350192576a28d455a79006b749a91fc9 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Tue, 2 Apr 2024 11:12:55 +0100 Subject: [PATCH 24/98] Split display of secure channel confirmation code and Device Authorization Grant user code Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/auth/LoginWithQR.tsx | 24 +++++++++++-------- src/components/views/auth/LoginWithQRFlow.tsx | 16 ++++++++++--- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index 2fb1b227b9c..42186c31ff3 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -52,6 +52,7 @@ interface IState { // MSC4108 verificationUri?: string; userCode?: string; + checkCode?: string; failureReason?: FailureReason; lastScannedCode?: Buffer; ourIntent: RendezvousIntent; @@ -245,19 +246,21 @@ export default class LoginWithQR extends React.Component { } }; - private approveLoginAfterShowingCode = async (checkCode: string | undefined): Promise => { + private approveLogin = async (checkCode: string | undefined): Promise => { if (!(this.state.rendezvous instanceof MSC4108SignInWithQR)) { throw new Error("Rendezvous not found"); } - if (!checkCode) { - throw new Error("No check code"); - } + if (!this.state.lastScannedCode) { + if (!checkCode) { + throw new Error("No check code"); + } - if (this.state.rendezvous?.checkCode !== checkCode) { - // PROTOTYPE: this doesn't actually show in the UI sensibly - // should we prompt to re-enter? - throw new Error("Check code mismatch"); + if (this.state.rendezvous?.checkCode !== checkCode) { + // PROTOTYPE: this doesn't actually show in the UI sensibly + // should we prompt to re-enter? + throw new Error("Check code mismatch"); + } } if (this.state.ourIntent === RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE) { @@ -292,6 +295,7 @@ export default class LoginWithQR extends React.Component { verificationUri: undefined, failureReason: undefined, userCode: undefined, + checkCode: undefined, homeserverBaseUrl: undefined, lastScannedCode: undefined, mediaPermissionError: false, @@ -306,7 +310,7 @@ export default class LoginWithQR extends React.Component { this.props.onFinished(false); break; case Click.Approve: - await (this.props.legacy ? this.legacyApproveLogin() : this.approveLoginAfterShowingCode(checkCode)); + await (this.props.legacy ? this.legacyApproveLogin() : this.approveLogin(checkCode)); break; case Click.Decline: await this.state.rendezvous?.declineLoginOnExistingDevice(); @@ -350,7 +354,7 @@ export default class LoginWithQR extends React.Component { failureReason={this.state.phase === Phase.Error ? this.state.failureReason : undefined} userCode={this.state.userCode} checkCode={ - this.state.phase === Phase.ShowChannelSecure ? this.state.rendezvous?.checkCode : undefined + this.state.checkCode } /> ); diff --git a/src/components/views/auth/LoginWithQRFlow.tsx b/src/components/views/auth/LoginWithQRFlow.tsx index 9ad3ac4c910..8958daa8422 100644 --- a/src/components/views/auth/LoginWithQRFlow.tsx +++ b/src/components/views/auth/LoginWithQRFlow.tsx @@ -224,13 +224,23 @@ export default class LoginWithQRFlow extends React.Component -

You’ll be asked to enter the following code on your other device:

-

{this.props.checkCode}

+

Go to your other device

+

You’ll be asked to enter the following code:

+

{this.props.checkCode}

+ + ); + break; + case Phase.ShowUserCode: + backButton = false; + main = ( + <> +

Go back to your other device to finish signing in

+
{this.props.userCode ? (

{_t("auth|qr_code_login|security_code")}

@@ -260,7 +270,7 @@ export default class LoginWithQRFlow extends React.Component ); break; - case Phase.ShowingQR: + case Phase.ShowingQR: if (this.props.code) { const data = typeof this.props.code !== "string" ? this.props.code : Buffer.from(this.props.code ?? ""); From 13045dab289e2a0f689b499fd6680b7dded8e161 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 9 Apr 2024 09:36:52 +0100 Subject: [PATCH 25/98] Iterate UX Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- res/css/_common.pcss | 6 +- res/css/views/auth/_LoginWithQR.pcss | 77 +++++----- .../views/auth/LoginWithQR-types.ts | 1 - src/components/views/auth/LoginWithQR.tsx | 49 +++---- src/components/views/auth/LoginWithQRFlow.tsx | 133 +++++------------- src/i18n/strings/en_EN.json | 32 ++--- 6 files changed, 119 insertions(+), 179 deletions(-) diff --git a/res/css/_common.pcss b/res/css/_common.pcss index 20ed9dfa392..c5a45886b3f 100644 --- a/res/css/_common.pcss +++ b/res/css/_common.pcss @@ -177,9 +177,9 @@ a:visited { color: $accent-alt; } -input[type="text"], -input[type="search"], -input[type="password"] { +:not(.mx_no_textinput) > input[type="text"], +:not(.mx_no_textinput) > input[type="search"], +:not(.mx_no_textinput) > input[type="password"] { padding: 9px; font: var(--cpd-font-body-md-semibold); font-weight: var(--cpd-font-weight-semibold); diff --git a/res/css/views/auth/_LoginWithQR.pcss b/res/css/views/auth/_LoginWithQR.pcss index 6a112c7c82c..e8c7b86cc73 100644 --- a/res/css/views/auth/_LoginWithQR.pcss +++ b/res/css/views/auth/_LoginWithQR.pcss @@ -32,28 +32,6 @@ limitations under the License. margin-top: $spacing-8; } - .mx_LoginWithQR_separator { - display: flex; - align-items: center; - text-align: center; - - &::before, - &::after { - content: ""; - flex: 1; - border-bottom: 1px solid $quinary-content; - } - - &:not(:empty) { - &::before { - margin-right: 1em; - } - &::after { - margin-left: 1em; - } - } - } - font-size: $font-15px; } @@ -69,18 +47,10 @@ limitations under the License. margin-bottom: 0; } - li { - line-height: 1.8; - } - .mx_QRCode { margin: $spacing-28 0; } - .mx_LoginWithQR_buttons { - text-align: center; - } - .mx_LoginWithQR_qrWrapper { display: flex; } @@ -137,14 +107,48 @@ limitations under the License. } ol { - list-style-position: inside; padding-inline-start: 0; + list-style: none; /* list markers do not support the outlined number styling we need */ + + li { + position: relative; + padding-left: 30px; + color: 1px solid $input-placeholder; + margin-bottom: 8px; + line-height: 20px; + } - li::marker { - color: $accent; + /* Circled number list item marker */ + li::before { + content: counter(list-item); + position: absolute; + left: 0; + display: inline-block; + width: 20px; + height: 20px; + line-height: 20px; + border-radius: 50%; + border: 1px solid $input-placeholder; + box-sizing: border-box; + text-align: center; } } + label[for="mx_LoginWithQR_checkCode"] { + margin-top: var(--cpd-space-6x); + color: var(--cpd-color-text-primary); + margin-bottom: 4px; + } + + .mx_LoginWithQR_checkCode { + margin-top: 4px; + height: 24px; + } + + .mx_LoginWithQR_checkCode_input { + margin-bottom: 4px; + } + .mx_LoginWithQR_heading { display: flex; gap: $spacing-12; @@ -164,13 +168,18 @@ limitations under the License. .mx_LoginWithQR_breadcrumbs { font-size: $font-13px; - color: var(--cpd-color-text-secondary); + color: $secondary-content; } .mx_LoginWithQR_main { display: flex; flex-direction: column; flex-grow: 1; + color: $primary-content; + + p { + color: $secondary-content; + } } .mx_QRCode { diff --git a/src/components/views/auth/LoginWithQR-types.ts b/src/components/views/auth/LoginWithQR-types.ts index 6ed9cc1cf8d..ec07f2473e9 100644 --- a/src/components/views/auth/LoginWithQR-types.ts +++ b/src/components/views/auth/LoginWithQR-types.ts @@ -27,7 +27,6 @@ export enum Mode { export enum Phase { Loading, ShowingQR, - Connecting, // The following are specific to MSC4108 OutOfBandConfirmation, WaitingForDevice, diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index 42186c31ff3..3513219d09a 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -16,20 +16,20 @@ limitations under the License. import React from "react"; import { - RendezvousFailureReason, - RendezvousIntent, - MSC4108SignInWithQR, - MSC4108SecureChannel, - MSC4108RendezvousSession, - MSC3906Rendezvous, MSC3886SimpleHttpRendezvousTransport, - MSC3903ECDHv2RendezvousChannel, MSC3903ECDHPayload, + MSC3903ECDHv2RendezvousChannel, + MSC3906Rendezvous, + MSC4108RendezvousSession, + MSC4108SecureChannel, + MSC4108SignInWithQR, + RendezvousFailureReason, + RendezvousIntent, } from "matrix-js-sdk/src/rendezvous"; import { logger } from "matrix-js-sdk/src/logger"; import { HTTPError, MatrixClient } from "matrix-js-sdk/src/matrix"; -import { Mode, Phase, Click } from "./LoginWithQR-types"; +import { Click, Mode, Phase } from "./LoginWithQR-types"; import LoginWithQRFlow from "./LoginWithQRFlow"; import { wrapRequestWithDialog } from "../../../utils/UserInteractiveAuth"; import { _t } from "../../../languageHandler"; @@ -59,16 +59,14 @@ interface IState { homeserverBaseUrl?: string; } -/** - * @deprecated the MSC3906 implementation is deprecated in favour of MSC4108. - */ export enum LoginWithQRFailureReason { + /** + * @deprecated the MSC3906 implementation is deprecated in favour of MSC4108. + */ RateLimited = "rate_limited", + CheckCodeMismatch = "check_code_mismatch", } -/** - * @deprecated the MSC3906 implementation is deprecated in favour of MSC4108. See {@see RendezvousFailureReason}. - */ export type FailureReason = RendezvousFailureReason | LoginWithQRFailureReason; /** @@ -248,19 +246,13 @@ export default class LoginWithQR extends React.Component { private approveLogin = async (checkCode: string | undefined): Promise => { if (!(this.state.rendezvous instanceof MSC4108SignInWithQR)) { + this.setState({ phase: Phase.Error, failureReason: RendezvousFailureReason.Unknown }); throw new Error("Rendezvous not found"); } - if (!this.state.lastScannedCode) { - if (!checkCode) { - throw new Error("No check code"); - } - - if (this.state.rendezvous?.checkCode !== checkCode) { - // PROTOTYPE: this doesn't actually show in the UI sensibly - // should we prompt to re-enter? - throw new Error("Check code mismatch"); - } + if (!this.state.lastScannedCode && this.state.rendezvous?.checkCode !== checkCode) { + this.setState({ failureReason: LoginWithQRFailureReason.CheckCodeMismatch }); + return; } if (this.state.ourIntent === RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE) { @@ -279,6 +271,7 @@ export default class LoginWithQR extends React.Component { // done this.props.onFinished(true); } else { + this.setState({ phase: Phase.Error, failureReason: RendezvousFailureReason.Unknown }); throw new Error("New device flows around OIDC are not yet implemented"); } }; @@ -341,7 +334,7 @@ export default class LoginWithQR extends React.Component { confirmationDigits={ this.state.phase === Phase.LegacyConnected ? this.state.confirmationDigits : undefined } - failureReason={this.state.phase === Phase.Error ? this.state.failureReason : undefined} + failureReason={this.state.failureReason} /> ); } @@ -351,11 +344,9 @@ export default class LoginWithQR extends React.Component { onClick={this.onClick} phase={this.state.phase} code={this.state.phase === Phase.ShowingQR ? this.state.rendezvous?.code : undefined} - failureReason={this.state.phase === Phase.Error ? this.state.failureReason : undefined} + failureReason={this.state.failureReason} userCode={this.state.userCode} - checkCode={ - this.state.checkCode - } + checkCode={this.state.checkCode} /> ); } diff --git a/src/components/views/auth/LoginWithQRFlow.tsx b/src/components/views/auth/LoginWithQRFlow.tsx index 8958daa8422..ce0bf2c6f51 100644 --- a/src/components/views/auth/LoginWithQRFlow.tsx +++ b/src/components/views/auth/LoginWithQRFlow.tsx @@ -14,20 +14,21 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { RefObject, createRef } from "react"; +import React, { createRef } from "react"; import { RendezvousFailureReason } from "matrix-js-sdk/src/rendezvous"; import { Icon as ChevronLeftIcon } from "@vector-im/compound-design-tokens/icons/chevron-left.svg"; +import { Heading, MFAInput, Text } from "@vector-im/compound-web"; import { _t } from "../../../languageHandler"; import AccessibleButton from "../elements/AccessibleButton"; import QRCode from "../elements/QRCode"; import Spinner from "../elements/Spinner"; import { Icon as InfoIcon } from "../../../../res/img/element-icons/i.svg"; -import { Icon as CheckmarkIcon } from "../../../../res/img/element-icons/check.svg"; import { Click, Phase } from "./LoginWithQR-types"; import SdkConfig from "../../../SdkConfig"; import { FailureReason, LoginWithQRFailureReason } from "./LoginWithQR"; import { XOR } from "../../../@types/common"; +import { ErrorMessage } from "../../structures/ErrorMessage"; /** * @deprecated the MSC3906 implementation is deprecated in favour of MSC4108. @@ -52,7 +53,7 @@ interface Props { * This supports the unstable features of MSC3906 and MSC4108 */ export default class LoginWithQRFlow extends React.Component> { - private checkCodeInput = createRef(null); + private checkCodeInput = createRef(); public constructor(props: XOR) { super(props); @@ -95,21 +96,12 @@ export default class LoginWithQRFlow extends React.Component -

- To verify that the connection is secure, please enter the code shown on your other device: -

-

- -

-
-
- -
-
- {this.props.userCode ? ( -
-

Security code

-

If asked, enter the code below on your other device.

-

{this.props.userCode}

-
- ) : null} - - ); - - buttons = ( - <> - - Continue - - - No code shown - - - ); - break; - case Phase.ShowChannelSecure: - backButton = false; - main = ( - <> -
-
- -
-
-

Go to your other device

-

You’ll be asked to enter the following code:

-

{this.props.checkCode}

- - ); - break; - case Phase.ShowUserCode: - backButton = false; - main = ( - <> -

Go back to your other device to finish signing in

-
- {this.props.userCode ? ( -
-

{_t("auth|qr_code_login|security_code")}

-

{_t("auth|qr_code_login|security_code_prompt")}

-

{this.props.userCode}

-
- ) : null} + + {_t("auth|qr_code_login|check_code_heading")} + + {_t("auth|qr_code_login|check_code_explainer")} + + + ); @@ -258,32 +208,31 @@ export default class LoginWithQRFlow extends React.Component - {_t("auth|qr_code_login|reject_action")} + {_t("action|cancel")} - {_t("auth|qr_code_login|confirm_action")} + {_t("action|continue")} ); break; - case Phase.ShowingQR: + case Phase.ShowingQR: if (this.props.code) { const data = typeof this.props.code !== "string" ? this.props.code : Buffer.from(this.props.code ?? ""); - const code = ( -
- -
- ); main = ( <> -

{_t("auth|qr_code_login|scan_code_instruction")}

- {code} + + {_t("auth|qr_code_login|scan_code_instruction")} + +
+ +
  1. {_t("auth|qr_code_login|open_element_other_device", { @@ -308,10 +257,6 @@ export default class LoginWithQRFlow extends React.Component diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index c8b3d53fb4d..c9e83f4429d 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -246,14 +246,14 @@ "phone_optional_label": "Phone (optional)", "qr_code_login": { "approve_access_warning": "By approving access for this device, it will have full access to your account.", + "check_code_explainer": "This will verify that the connection to your other device is secure.", + "check_code_heading": "Enter the number shown on your other device", + "check_code_input_label": "2-digit code", + "check_code_mismatch": "The numbers don't match", "completing_setup": "Completing set up of your new device", "confirm_code_match": "Check that the code below matches with your other device:", - "connecting": "Connecting…", - "error_device_already_signed_in": "The other device is already signed in.", - "error_device_not_signed_in": "The other device isn't signed in.", "error_device_unsupported": "Linking with this device is not supported.", "error_homeserver_lacks_support": "The homeserver doesn't support signing in another device.", - "error_invalid_scanned_code": "The scanned code is invalid.", "error_linking_incomplete": "The linking wasn't completed in the required time.", "error_rate_limited": "Too many attempts in a short time. Wait some time before trying again.", "error_request_cancelled": "The request was cancelled.", @@ -263,16 +263,12 @@ "open_element_other_device": "Open %(brand)s on your other device", "point_the_camera": "Point the camera at the QR code shown here", "scan_code_instruction": "Scan the QR code with another device", - "scan_qr_code": "Scan QR code", + "scan_qr_code": "Sign in with QR code", + "security_code": "Security code", + "security_code_prompt": "If asked, enter the code below on your other device.", "select_qr_code": "Select \"%(scanQRCode)s\"", "sign_in_new_device": "Sign in new device", - "waiting_for_device": "Waiting for device to sign in", - "security_code_prompt": "If asked, enter the code below on your other device.", - "security_code": "Security code", - "reject_action": "No, I don't", - "confirm_green_checkmark_title": "Do you see a green checkmark on your other device?", - "confirm_green_checkmark": "Confirm that you see a green checkmark on both devices to to verify that the connection is secure.", - "confirm_action": "Yes, I do" + "waiting_for_device": "Waiting for device to sign in" }, "register_action": "Create Account", "registration": { @@ -1423,6 +1419,7 @@ "group_spaces": "Spaces", "group_themes": "Themes", "group_threads": "Threads", + "group_ui": "User interface", "group_voip": "Voice & Video", "group_widgets": "Widgets", "hidebold": "Hide notification dot (only display counters badges)", @@ -1446,6 +1443,7 @@ "oidc_native_flow": "OIDC native authentication", "oidc_native_flow_description": "⚠ WARNING: Experimental. Use OIDC native authentication when supported by the server.", "pinning": "Message Pinning", + "release_announcement": "Release announcement", "render_reaction_images": "Render custom images in reactions", "render_reaction_images_description": "Sometimes referred to as \"custom emojis\".", "report_to_moderators": "Report to moderators", @@ -1481,9 +1479,7 @@ "video_rooms_feedbackSubheading": "Thank you for trying the beta, please go into as much detail as you can so we can improve it.", "voice_broadcast": "Voice broadcast", "voice_broadcast_force_small_chunks": "Force 15s voice broadcast chunk length", - "wysiwyg_composer": "Rich text editor", - "release_announcement": "Release announcement", - "group_ui": "User interface" + "wysiwyg_composer": "Rich text editor" }, "labs_mjolnir": { "advanced_warning": "⚠ These settings are meant for advanced users.", @@ -3170,8 +3166,8 @@ "threads_activity_centre": { "header": "Threads activity", "no_rooms_with_unreads_threads": "You don't have rooms with unread threads yet.", - "release_announcement_header": "Threads Activity Centre", - "release_announcement_description": "Threads notifications have moved, find them here from now on." + "release_announcement_description": "Threads notifications have moved, find them here from now on.", + "release_announcement_header": "Threads Activity Centre" }, "time": { "about_day_ago": "about a day ago", @@ -4060,4 +4056,4 @@ "wordByItself": "A word by itself is easy to guess" } } -} \ No newline at end of file +} From f30dc40ec5e17933b1f08fd84e59b7037eadd8d1 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 10 Apr 2024 14:46:05 +0100 Subject: [PATCH 26/98] Handle additional rendezvous failure reasons Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/auth/LoginWithQR.tsx | 28 ++++++----- src/components/views/auth/LoginWithQRFlow.tsx | 15 +++++- src/i18n/strings/en_EN.json | 46 ++++++++++--------- 3 files changed, 52 insertions(+), 37 deletions(-) diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index 3513219d09a..752bd28631c 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -235,12 +235,9 @@ export default class LoginWithQR extends React.Component { } // we ask the user to confirm that the channel is secure - } catch (e) { - logger.error("Error whilst doing QR login", e); - // only set to error phase if it hasn't already been set by onFailure or similar - if (this.state.phase !== Phase.Error) { - this.setState({ phase: Phase.Error, failureReason: RendezvousFailureReason.Unknown }); - } + } catch (e: RendezvousError | unknown) { + logger.error("Error whilst generating QR", e); + await this.state.rendezvous?.cancel(e instanceof RendezvousError ? e.code : RendezvousFailureReason.Unknown); } }; @@ -255,18 +252,19 @@ export default class LoginWithQR extends React.Component { return; } - if (this.state.ourIntent === RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE) { - // MSC4108-Flow: NewScanned - this.setState({ phase: Phase.Loading }); + try { + if (this.state.ourIntent === RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE) { + // MSC4108-Flow: NewScanned + this.setState({ phase: Phase.Loading }); - if (this.state.verificationUri) { - window.open(this.state.verificationUri, "_blank"); - } + if (this.state.verificationUri) { + window.open(this.state.verificationUri, "_blank"); + } - this.setState({ phase: Phase.WaitingForDevice }); + this.setState({ phase: Phase.WaitingForDevice }); - // send secrets - await this.state.rendezvous.loginStep5(); + // send secrets + await this.state.rendezvous.loginStep5(); // done this.props.onFinished(true); diff --git a/src/components/views/auth/LoginWithQRFlow.tsx b/src/components/views/auth/LoginWithQRFlow.tsx index ce0bf2c6f51..bdb181678eb 100644 --- a/src/components/views/auth/LoginWithQRFlow.tsx +++ b/src/components/views/auth/LoginWithQRFlow.tsx @@ -96,6 +96,7 @@ export default class LoginWithQRFlow extends React.ComponentTip: Use “%(replyInThread)s” when hovering over a message.", "error_start_thread_existing_relation": "Can't create a thread from an event with an existing relation", - "mark_all_read": "Mark all as read", "my_threads": "My threads", "my_threads_description": "Shows all threads you've participated in", "open_thread": "Open thread", "show_all_threads": "Show all threads", "show_thread_filter": "Show:", - "unable_to_decrypt": "Unable to decrypt message" + "unable_to_decrypt": "Unable to decrypt message", + "mark_all_read": "Mark all as read" }, "threads_activity_centre": { "header": "Threads activity", "no_rooms_with_unreads_threads": "You don't have rooms with unread threads yet.", - "release_announcement_description": "Threads notifications have moved, find them here from now on.", - "release_announcement_header": "Threads Activity Centre" + "release_announcement_header": "Threads Activity Centre", + "release_announcement_description": "Threads notifications have moved, find them here from now on." }, "time": { "about_day_ago": "about a day ago", @@ -3846,9 +3850,6 @@ "legacy_call": "Legacy Call", "maximise": "Fill screen", "maximise_call": "Maximise call", - "metaspace_video_rooms": { - "conference_room_section": "Conferences" - }, "minimise_call": "Minimise call", "misconfigured_server": "Call failed due to misconfigured server", "misconfigured_server_description": "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.", @@ -3899,7 +3900,10 @@ "video_call_started": "Video call started", "video_call_using": "Video call using:", "voice_call": "Voice call", - "you_are_presenting": "You are presenting" + "you_are_presenting": "You are presenting", + "metaspace_video_rooms": { + "conference_room_section": "Conferences" + } }, "widget": { "added_by": "Widget added by", From 437bbcb75a06b57062c5d81a2b5fbc3bc1807c09 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 10 Apr 2024 14:52:35 +0100 Subject: [PATCH 27/98] Rename data_mismatch to insecure_channel_detected --- src/components/views/auth/LoginWithQRFlow.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/auth/LoginWithQRFlow.tsx b/src/components/views/auth/LoginWithQRFlow.tsx index bdb181678eb..74d6d604dfd 100644 --- a/src/components/views/auth/LoginWithQRFlow.tsx +++ b/src/components/views/auth/LoginWithQRFlow.tsx @@ -115,7 +115,7 @@ export default class LoginWithQRFlow extends React.Component Date: Wed, 10 Apr 2024 16:24:16 +0100 Subject: [PATCH 28/98] Update failure reasons to match MSC Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/auth/LoginWithQR.tsx | 10 +++---- src/components/views/auth/LoginWithQRFlow.tsx | 29 +++++++++++-------- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index 752bd28631c..1b37994447b 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -118,7 +118,7 @@ export default class LoginWithQR extends React.Component { // eslint-disable-next-line react/no-direct-mutation-state this.state.rendezvous.onFailure = undefined; // calling cancel will call close() as well to clean up the resources - this.state.rendezvous.cancel(RendezvousFailureReason.UserCancelled).then(() => {}); + this.state.rendezvous.cancel(MSC4108FailureReason.UserCancelled).then(() => {}); } } @@ -203,7 +203,7 @@ export default class LoginWithQR extends React.Component { }); } catch (e) { logger.error("Error whilst generating QR code", e); - this.setState({ phase: Phase.Error, failureReason: RendezvousFailureReason.HomeserverLacksSupport }); + this.setState({ phase: Phase.Error, failureReason: ClientRendezvousFailureReason.HomeserverLacksSupport }); return; } @@ -237,7 +237,7 @@ export default class LoginWithQR extends React.Component { // we ask the user to confirm that the channel is secure } catch (e: RendezvousError | unknown) { logger.error("Error whilst generating QR", e); - await this.state.rendezvous?.cancel(e instanceof RendezvousError ? e.code : RendezvousFailureReason.Unknown); + await this.state.rendezvous?.cancel(e instanceof RendezvousError ? e.code : ClientRendezvousFailureReason.Unknown); } }; @@ -296,7 +296,7 @@ export default class LoginWithQR extends React.Component { private onClick = async (type: Click, checkCode?: string): Promise => { switch (type) { case Click.Cancel: - await this.state.rendezvous?.cancel(RendezvousFailureReason.UserCancelled); + await this.state.rendezvous?.cancel(MSC4108FailureReason.UserCancelled); this.reset(); this.props.onFinished(false); break; @@ -313,7 +313,7 @@ export default class LoginWithQR extends React.Component { await this.updateMode(this.props.mode); break; case Click.Back: - await this.state.rendezvous?.cancel(RendezvousFailureReason.UserCancelled); + await this.state.rendezvous?.cancel(MSC4108FailureReason.UserCancelled); this.props.onFinished(false); break; case Click.ShowQr: diff --git a/src/components/views/auth/LoginWithQRFlow.tsx b/src/components/views/auth/LoginWithQRFlow.tsx index 74d6d604dfd..bc2b6e72e79 100644 --- a/src/components/views/auth/LoginWithQRFlow.tsx +++ b/src/components/views/auth/LoginWithQRFlow.tsx @@ -15,7 +15,7 @@ limitations under the License. */ import React, { createRef } from "react"; -import { RendezvousFailureReason } from "matrix-js-sdk/src/rendezvous"; +import { MSC4108FailureReason, ClientRendezvousFailureReason } from "matrix-js-sdk/src/rendezvous"; import { Icon as ChevronLeftIcon } from "@vector-im/compound-design-tokens/icons/chevron-left.svg"; import { Heading, MFAInput, Text } from "@vector-im/compound-web"; @@ -93,38 +93,43 @@ export default class LoginWithQRFlow extends React.Component Date: Mon, 15 Apr 2024 15:14:31 +0100 Subject: [PATCH 29/98] Test deeper Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../views/auth/LoginWithQR-types.ts | 1 + src/components/views/auth/LoginWithQR.tsx | 48 ++++++++++++++----- .../settings/devices/LoginWithQR-test.tsx | 14 ++++-- .../settings/devices/LoginWithQRFlow-test.tsx | 5 +- 4 files changed, 48 insertions(+), 20 deletions(-) diff --git a/src/components/views/auth/LoginWithQR-types.ts b/src/components/views/auth/LoginWithQR-types.ts index ec07f2473e9..1877ff653fb 100644 --- a/src/components/views/auth/LoginWithQR-types.ts +++ b/src/components/views/auth/LoginWithQR-types.ts @@ -28,6 +28,7 @@ export enum Phase { Loading, ShowingQR, // The following are specific to MSC4108 + Connecting, OutOfBandConfirmation, WaitingForDevice, Verifying, diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index 1b37994447b..ca55bf289c8 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -16,13 +16,17 @@ limitations under the License. import React from "react"; import { + ClientRendezvousFailureReason, + LegacyRendezvousFailureReason, MSC3886SimpleHttpRendezvousTransport, MSC3903ECDHPayload, MSC3903ECDHv2RendezvousChannel, MSC3906Rendezvous, + MSC4108FailureReason, MSC4108RendezvousSession, MSC4108SecureChannel, MSC4108SignInWithQR, + RendezvousError, RendezvousFailureReason, RendezvousIntent, } from "matrix-js-sdk/src/rendezvous"; @@ -104,7 +108,7 @@ export default class LoginWithQR extends React.Component { const rendezvous = this.state.rendezvous; rendezvous.onFailure = undefined; if (this.props.legacy) { - await rendezvous.cancel(RendezvousFailureReason.UserCancelled); + await rendezvous.cancel(LegacyRendezvousFailureReason.UserCancelled); } this.setState({ rendezvous: undefined }); } @@ -118,7 +122,11 @@ export default class LoginWithQR extends React.Component { // eslint-disable-next-line react/no-direct-mutation-state this.state.rendezvous.onFailure = undefined; // calling cancel will call close() as well to clean up the resources - this.state.rendezvous.cancel(MSC4108FailureReason.UserCancelled).then(() => {}); + if (this.state.rendezvous instanceof MSC3906Rendezvous) { + this.state.rendezvous.cancel(LegacyRendezvousFailureReason.UserCancelled); + } else { + this.state.rendezvous.cancel(MSC4108FailureReason.UserCancelled); + } } } @@ -167,7 +175,7 @@ export default class LoginWithQR extends React.Component { this.setState({ phase: Phase.Error, failureReason: LoginWithQRFailureReason.RateLimited }); return; } - this.setState({ phase: Phase.Error, failureReason: RendezvousFailureReason.Unknown }); + this.setState({ phase: Phase.Error, failureReason: ClientRendezvousFailureReason.Unknown }); } } @@ -236,14 +244,16 @@ export default class LoginWithQR extends React.Component { // we ask the user to confirm that the channel is secure } catch (e: RendezvousError | unknown) { - logger.error("Error whilst generating QR", e); - await this.state.rendezvous?.cancel(e instanceof RendezvousError ? e.code : ClientRendezvousFailureReason.Unknown); + logger.error("Error whilst approving login", e); + await this.state.rendezvous?.cancel( + e instanceof RendezvousError ? e.code : ClientRendezvousFailureReason.Unknown, + ); } }; private approveLogin = async (checkCode: string | undefined): Promise => { if (!(this.state.rendezvous instanceof MSC4108SignInWithQR)) { - this.setState({ phase: Phase.Error, failureReason: RendezvousFailureReason.Unknown }); + this.setState({ phase: Phase.Error, failureReason: ClientRendezvousFailureReason.Unknown }); throw new Error("Rendezvous not found"); } @@ -266,11 +276,15 @@ export default class LoginWithQR extends React.Component { // send secrets await this.state.rendezvous.loginStep5(); - // done - this.props.onFinished(true); - } else { - this.setState({ phase: Phase.Error, failureReason: RendezvousFailureReason.Unknown }); - throw new Error("New device flows around OIDC are not yet implemented"); + // done + this.props.onFinished(true); + } else { + this.setState({ phase: Phase.Error, failureReason: ClientRendezvousFailureReason.Unknown }); + throw new Error("New device flows around OIDC are not yet implemented"); + } + } catch (e: RendezvousError | unknown) { + logger.error("Error whilst approving sign in", e); + this.setState({ phase: Phase.Error, failureReason: ClientRendezvousFailureReason.Unknown }); } }; @@ -296,7 +310,11 @@ export default class LoginWithQR extends React.Component { private onClick = async (type: Click, checkCode?: string): Promise => { switch (type) { case Click.Cancel: - await this.state.rendezvous?.cancel(MSC4108FailureReason.UserCancelled); + if (this.state.rendezvous instanceof MSC3906Rendezvous) { + await this.state.rendezvous?.cancel(LegacyRendezvousFailureReason.UserCancelled); + } else { + await this.state.rendezvous?.cancel(MSC4108FailureReason.UserCancelled); + } this.reset(); this.props.onFinished(false); break; @@ -313,7 +331,11 @@ export default class LoginWithQR extends React.Component { await this.updateMode(this.props.mode); break; case Click.Back: - await this.state.rendezvous?.cancel(MSC4108FailureReason.UserCancelled); + if (this.state.rendezvous instanceof MSC3906Rendezvous) { + await this.state.rendezvous?.cancel(LegacyRendezvousFailureReason.UserCancelled); + } else { + await this.state.rendezvous?.cancel(MSC4108FailureReason.UserCancelled); + } this.props.onFinished(false); break; case Click.ShowQr: diff --git a/test/components/views/settings/devices/LoginWithQR-test.tsx b/test/components/views/settings/devices/LoginWithQR-test.tsx index 1ec19da6eab..772245f608d 100644 --- a/test/components/views/settings/devices/LoginWithQR-test.tsx +++ b/test/components/views/settings/devices/LoginWithQR-test.tsx @@ -17,7 +17,11 @@ limitations under the License. import { cleanup, render, waitFor } from "@testing-library/react"; import { MockedObject, mocked } from "jest-mock"; import React from "react"; -import { MSC3906Rendezvous, RendezvousFailureReason } from "matrix-js-sdk/src/rendezvous"; +import { + MSC3906Rendezvous, + LegacyRendezvousFailureReason, + ClientRendezvousFailureReason, +} from "matrix-js-sdk/src/rendezvous"; import { HTTPError, LoginTokenPostResponse } from "matrix-js-sdk/src/matrix"; import LoginWithQR from "../../../../../src/components/views/auth/LoginWithQR"; @@ -112,7 +116,7 @@ describe("", () => { await waitFor(() => expect(mockedFlow).toHaveBeenLastCalledWith({ phase: Phase.Error, - failureReason: RendezvousFailureReason.HomeserverLacksSupport, + failureReason: LegacyRendezvousFailureReason.HomeserverLacksSupport, onClick: expect.any(Function), }), ); @@ -126,7 +130,7 @@ describe("", () => { await waitFor(() => expect(mockedFlow).toHaveBeenLastCalledWith({ phase: Phase.Error, - failureReason: RendezvousFailureReason.Unknown, + failureReason: ClientRendezvousFailureReason.Unknown, onClick: expect.any(Function), }), ); @@ -161,7 +165,7 @@ describe("", () => { const onClick = mockedFlow.mock.calls[0][0].onClick; await onClick(Click.Cancel); expect(onFinished).toHaveBeenCalledWith(false); - expect(rendezvous.cancel).toHaveBeenCalledWith(RendezvousFailureReason.UserCancelled); + expect(rendezvous.cancel).toHaveBeenCalledWith(LegacyRendezvousFailureReason.UserCancelled); // try again onClick(Click.TryAgain); @@ -206,7 +210,7 @@ describe("", () => { const onClick = mockedFlow.mock.calls[0][0].onClick; await onClick(Click.Back); expect(onFinished).toHaveBeenCalledWith(false); - expect(rendezvous.cancel).toHaveBeenCalledWith(RendezvousFailureReason.UserCancelled); + expect(rendezvous.cancel).toHaveBeenCalledWith(LegacyRendezvousFailureReason.UserCancelled); }); test("render QR then decline", async () => { diff --git a/test/components/views/settings/devices/LoginWithQRFlow-test.tsx b/test/components/views/settings/devices/LoginWithQRFlow-test.tsx index 702dc447af0..5f0fa4cdff6 100644 --- a/test/components/views/settings/devices/LoginWithQRFlow-test.tsx +++ b/test/components/views/settings/devices/LoginWithQRFlow-test.tsx @@ -16,7 +16,7 @@ limitations under the License. import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; import React from "react"; -import { RendezvousFailureReason } from "matrix-js-sdk/src/rendezvous"; +import { LegacyRendezvousFailureReason, MSC4108FailureReason } from "matrix-js-sdk/src/rendezvous"; import LoginWithQRFlow from "../../../../../src/components/views/auth/LoginWithQRFlow"; import { LoginWithQRFailureReason, FailureReason } from "../../../../../src/components/views/auth/LoginWithQR"; @@ -99,7 +99,8 @@ describe("", () => { describe("errors", () => { for (const failureReason of [ - ...Object.values(RendezvousFailureReason), + ...Object.values(LegacyRendezvousFailureReason), + ...Object.values(MSC4108FailureReason), ...Object.values(LoginWithQRFailureReason), ]) { it(`renders ${failureReason}`, async () => { From 41f9178440d7eb878a4e1818683976630e01e2ab Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 17 Apr 2024 13:48:10 +0100 Subject: [PATCH 30/98] Fix types Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/auth/LoginWithQR.tsx | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index ca55bf289c8..60aa2ce018c 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -107,7 +107,7 @@ export default class LoginWithQR extends React.Component { if (this.state.rendezvous) { const rendezvous = this.state.rendezvous; rendezvous.onFailure = undefined; - if (this.props.legacy) { + if (rendezvous instanceof MSC3906Rendezvous) { await rendezvous.cancel(LegacyRendezvousFailureReason.UserCancelled); } this.setState({ rendezvous: undefined }); @@ -245,9 +245,19 @@ export default class LoginWithQR extends React.Component { // we ask the user to confirm that the channel is secure } catch (e: RendezvousError | unknown) { logger.error("Error whilst approving login", e); - await this.state.rendezvous?.cancel( - e instanceof RendezvousError ? e.code : ClientRendezvousFailureReason.Unknown, - ); + if (this.state.rendezvous instanceof MSC3906Rendezvous) { + await this.state.rendezvous?.cancel( + e instanceof RendezvousError + ? (e.code as LegacyRendezvousFailureReason) + : LegacyRendezvousFailureReason.Unknown, + ); + } else { + await this.state.rendezvous?.cancel( + e instanceof RendezvousError + ? (e.code as MSC4108FailureReason) + : ClientRendezvousFailureReason.Unknown, + ); + } } }; From 38b3882a3a401aebc5d82bd90855b9d6d9c21e15 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 17 Apr 2024 13:48:47 +0100 Subject: [PATCH 31/98] i18n Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/i18n/strings/en_EN.json | 74 +++++++++++++++++++------------------ 1 file changed, 38 insertions(+), 36 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 415f332418a..5dfe46ab9b1 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -245,34 +245,36 @@ "phone_label": "Phone", "phone_optional_label": "Phone (optional)", "qr_code_login": { + "approve_access_warning": "By approving access for this device, it will have full access to your account.", + "check_code_explainer": "This will verify that the connection to your other device is secure.", + "check_code_heading": "Enter the number shown on your other device", + "check_code_input_label": "2-digit code", + "check_code_mismatch": "The numbers don't match", "completing_setup": "Completing set up of your new device", + "confirm_code_match": "Check that the code below matches with your other device:", + "error_device_already_exists": "The specified device is already signed in.", + "error_device_already_signed_in": "The other device is already signed in.", + "error_device_not_found": "The new device does not appear to have signed in.", + "error_device_not_signed_in": "The other device isn't signed in.", "error_device_unsupported": "Linking with this device is not supported.", "error_homeserver_lacks_support": "The homeserver doesn't support signing in another device.", + "error_insecure_connection": "Connection not secure.", "error_linking_incomplete": "The linking wasn't completed in the required time.", + "error_rate_limited": "Too many attempts in a short time. Wait some time before trying again.", "error_request_cancelled": "The request was cancelled.", "error_request_declined": "The request was declined.", "error_unexpected": "An unexpected error occurred.", + "error_unexpected_message": "An unexpected message was received from the other device.", "follow_remaining_instructions": "Follow the remaining instructions to verify your other device", "open_element_other_device": "Open %(brand)s on your other device", "point_the_camera": "Point the camera at the QR code shown here", "scan_code_instruction": "Scan the QR code with another device", "scan_qr_code": "Sign in with QR code", + "security_code": "Security code", + "security_code_prompt": "If asked, enter the code below on your other device.", "select_qr_code": "Select \"%(scanQRCode)s\"", - "waiting_for_device": "Waiting for device to sign in", "sign_in_new_device": "Sign in new device", - "security_code_prompt": "If asked, enter the code below on your other device.", - "security_code": "Security code", - "error_unexpected_message": "An unexpected message was received from the other device.", - "error_rate_limited": "Too many attempts in a short time. Wait some time before trying again.", - "error_insecure_connection": "Connection not secure.", - "error_device_not_found": "The new device does not appear to have signed in.", - "error_device_already_exists": "The specified device is already signed in.", - "confirm_code_match": "Check that the code below matches with your other device:", - "check_code_mismatch": "The numbers don't match", - "check_code_input_label": "2-digit code", - "check_code_heading": "Enter the number shown on your other device", - "check_code_explainer": "This will verify that the connection to your other device is secure.", - "approve_access_warning": "By approving access for this device, it will have full access to your account." + "waiting_for_device": "Waiting for device to sign in" }, "register_action": "Create Account", "registration": { @@ -1423,6 +1425,7 @@ "group_spaces": "Spaces", "group_themes": "Themes", "group_threads": "Threads", + "group_ui": "User interface", "group_voip": "Voice & Video", "group_widgets": "Widgets", "hidebold": "Hide notification dot (only display counters badges)", @@ -1446,6 +1449,7 @@ "oidc_native_flow": "OIDC native authentication", "oidc_native_flow_description": "⚠ WARNING: Experimental. Use OIDC native authentication when supported by the server.", "pinning": "Message Pinning", + "release_announcement": "Release announcement", "render_reaction_images": "Render custom images in reactions", "render_reaction_images_description": "Sometimes referred to as \"custom emojis\".", "report_to_moderators": "Report to moderators", @@ -1481,9 +1485,7 @@ "video_rooms_feedbackSubheading": "Thank you for trying the beta, please go into as much detail as you can so we can improve it.", "voice_broadcast": "Voice broadcast", "voice_broadcast_force_small_chunks": "Force 15s voice broadcast chunk length", - "wysiwyg_composer": "Rich text editor", - "release_announcement": "Release announcement", - "group_ui": "User interface" + "wysiwyg_composer": "Rich text editor" }, "labs_mjolnir": { "advanced_warning": "⚠ These settings are meant for advanced users.", @@ -2861,11 +2863,11 @@ "metaspaces_orphans_description": "Group all your rooms that aren't part of a space in one place.", "metaspaces_people_description": "Group all your people in one place.", "metaspaces_subsection": "Spaces to show", - "spaces_explainer": "Spaces are ways to group rooms and people. Alongside the spaces you're in, you can use some pre-built ones too.", - "title": "Sidebar", - "metaspaces_video_rooms_description_invite_extension": "In conferences you can invite people outside of matrix.", + "metaspaces_video_rooms": "Video rooms and conferences", "metaspaces_video_rooms_description": "Group all private video rooms and conferences.", - "metaspaces_video_rooms": "Video rooms and conferences" + "metaspaces_video_rooms_description_invite_extension": "In conferences you can invite people outside of matrix.", + "spaces_explainer": "Spaces are ways to group rooms and people. Alongside the spaces you're in, you can use some pre-built ones too.", + "title": "Sidebar" }, "start_automatically": "Start automatically after system login", "use_12_hour_format": "Show timestamps in 12 hour format (e.g. 2:30pm)", @@ -3159,19 +3161,19 @@ "empty_heading": "Keep discussions organised with threads", "empty_tip": "Tip: Use “%(replyInThread)s” when hovering over a message.", "error_start_thread_existing_relation": "Can't create a thread from an event with an existing relation", + "mark_all_read": "Mark all as read", "my_threads": "My threads", "my_threads_description": "Shows all threads you've participated in", "open_thread": "Open thread", "show_all_threads": "Show all threads", "show_thread_filter": "Show:", - "unable_to_decrypt": "Unable to decrypt message", - "mark_all_read": "Mark all as read" + "unable_to_decrypt": "Unable to decrypt message" }, "threads_activity_centre": { "header": "Threads activity", "no_rooms_with_unreads_threads": "You don't have rooms with unread threads yet.", - "release_announcement_header": "Threads Activity Centre", - "release_announcement_description": "Threads notifications have moved, find them here from now on." + "release_announcement_description": "Threads notifications have moved, find them here from now on.", + "release_announcement_header": "Threads Activity Centre" }, "time": { "about_day_ago": "about a day ago", @@ -3668,6 +3670,12 @@ "toast_title": "Update %(brand)s", "unavailable": "Unavailable" }, + "update_room_access_modal": { + "description": "To create a share link, you need to allow guests to join this room. This may make the room less secure. When you're done with the call, you can make the room private again.", + "dont_change_description": "Alternatively, you can hold the call in a separate room.", + "no_change": "I don't want to change the access level.", + "title": "Change the room access level" + }, "upload_failed_generic": "The file '%(fileName)s' failed to upload.", "upload_failed_size": "The file '%(fileName)s' exceeds this homeserver's size limit for uploads", "upload_failed_title": "Upload Failed", @@ -3850,6 +3858,9 @@ "legacy_call": "Legacy Call", "maximise": "Fill screen", "maximise_call": "Maximise call", + "metaspace_video_rooms": { + "conference_room_section": "Conferences" + }, "minimise_call": "Minimise call", "misconfigured_server": "Call failed due to misconfigured server", "misconfigured_server_description": "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.", @@ -3900,10 +3911,7 @@ "video_call_started": "Video call started", "video_call_using": "Video call using:", "voice_call": "Voice call", - "you_are_presenting": "You are presenting", - "metaspace_video_rooms": { - "conference_room_section": "Conferences" - } + "you_are_presenting": "You are presenting" }, "widget": { "added_by": "Widget added by", @@ -4059,11 +4067,5 @@ "userInputs": "There should not be any personal or page related data.", "wordByItself": "A word by itself is easy to guess" } - }, - "update_room_access_modal": { - "description": "To create a share link, you need to allow guests to join this room. This may make the room less secure. When you're done with the call, you can make the room private again.", - "dont_change_description": "Alternatively, you can hold the call in a separate room.", - "no_change": "I don't want to change the access level.", - "title": "Change the room access level" } -} \ No newline at end of file +} From e32136fd261fd8d783fefcd87eb33ba59ae010e0 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 17 Apr 2024 17:57:50 +0100 Subject: [PATCH 32/98] Gate OIDC QR on OIDC Native labs flag Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/settings/devices/LoginWithQRSection.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/views/settings/devices/LoginWithQRSection.tsx b/src/components/views/settings/devices/LoginWithQRSection.tsx index 3932f4f9c74..f1c297736a3 100644 --- a/src/components/views/settings/devices/LoginWithQRSection.tsx +++ b/src/components/views/settings/devices/LoginWithQRSection.tsx @@ -29,6 +29,8 @@ import { logger } from "matrix-js-sdk/src/logger"; import { _t } from "../../../../languageHandler"; import AccessibleButton from "../../elements/AccessibleButton"; import SettingsSubsection from "../shared/SettingsSubsection"; +import SettingsStore from "../../../../settings/SettingsStore"; +import { Features } from "../../../../settings/Settings"; interface IProps { onShowQr: () => void; @@ -69,7 +71,7 @@ export default class LoginWithQRSection extends React.Component { deviceAuthorizationGrantSupported, }); const offerShowQr = this.props.oidcClientConfig - ? deviceAuthorizationGrantSupported && msc4108Supported + ? deviceAuthorizationGrantSupported && msc4108Supported && SettingsStore.getValue(Features.OidcNativeFlow) : getLoginTokenSupported && msc3886Supported; // don't show anything if no method is available From 62d83739607fc4eebb06fc399c9c2904653bd3e7 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 18 Apr 2024 11:46:55 +0100 Subject: [PATCH 33/98] Iterate QR OIDC UX Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- res/css/views/auth/_LoginWithQR.pcss | 54 +++++- .../views/auth/LoginWithQR-types.ts | 1 - src/components/views/auth/LoginWithQR.tsx | 4 - src/components/views/auth/LoginWithQRFlow.tsx | 177 ++++++++++-------- src/i18n/strings/en_EN.json | 31 +-- .../settings/devices/LoginWithQR-test.tsx | 45 ----- .../settings/devices/LoginWithQRFlow-test.tsx | 2 - 7 files changed, 165 insertions(+), 149 deletions(-) diff --git a/res/css/views/auth/_LoginWithQR.pcss b/res/css/views/auth/_LoginWithQR.pcss index e8c7b86cc73..5fb35a86a17 100644 --- a/res/css/views/auth/_LoginWithQR.pcss +++ b/res/css/views/auth/_LoginWithQR.pcss @@ -47,6 +47,10 @@ limitations under the License. margin-bottom: 0; } + h2 { + margin-top: $spacing-24; + } + .mx_QRCode { margin: $spacing-28 0; } @@ -61,12 +65,6 @@ limitations under the License. display: flex; flex-direction: column; - .mx_LoginWithQR_centreTitle { - h1 { - text-align: center; - } - } - h1 > svg { &.normal { color: $secondary-content; @@ -114,7 +112,7 @@ limitations under the License. position: relative; padding-left: 30px; color: 1px solid $input-placeholder; - margin-bottom: 8px; + margin-bottom: 16px; line-height: 20px; } @@ -140,6 +138,27 @@ limitations under the License. margin-bottom: 4px; } + .mx_LoginWithQR_icon { + width: 56px; + height: 56px; + border-radius: 8px; + box-sizing: border-box; + padding: 12px; + gap: 10px; + + background-color: var(--cpd-color-bg-success-subtle); + svg { + color: var(--cpd-color-icon-success-primary); + } + + &.mx_LoginWithQR_icon--critical { + background-color: var(--cpd-color-bg-critical-subtle); + svg { + color: var(--cpd-color-icon-critical-primary); + } + } + } + .mx_LoginWithQR_checkCode { margin-top: 4px; height: 24px; @@ -175,13 +194,34 @@ limitations under the License. display: flex; flex-direction: column; flex-grow: 1; + align-items: center; color: $primary-content; + text-align: center; p { color: $secondary-content; } } + &.mx_LoginWithQR_error .mx_LoginWithQR_main { + max-width: 400px; + margin: 0 auto; + } + + .mx_LoginWithQR_buttons { + display: flex; + flex-direction: column; + align-items: center; + gap: $spacing-16; + margin-top: 24px; + + .mx_AccessibleButton { + width: 300px; + height: 48px; + box-sizing: border-box; + } + } + .mx_QRCode { border-radius: $spacing-8; display: flex; diff --git a/src/components/views/auth/LoginWithQR-types.ts b/src/components/views/auth/LoginWithQR-types.ts index 1877ff653fb..3fede49db41 100644 --- a/src/components/views/auth/LoginWithQR-types.ts +++ b/src/components/views/auth/LoginWithQR-types.ts @@ -43,7 +43,6 @@ export enum Click { Cancel, Decline, Approve, - TryAgain, Back, ShowQr, } diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index 60aa2ce018c..32a03977d19 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -336,10 +336,6 @@ export default class LoginWithQR extends React.Component { this.reset(); this.props.onFinished(false); break; - case Click.TryAgain: - this.reset(); - await this.updateMode(this.props.mode); - break; case Click.Back: if (this.state.rendezvous instanceof MSC3906Rendezvous) { await this.state.rendezvous?.cancel(LegacyRendezvousFailureReason.UserCancelled); diff --git a/src/components/views/auth/LoginWithQRFlow.tsx b/src/components/views/auth/LoginWithQRFlow.tsx index bc2b6e72e79..8baed9cf64c 100644 --- a/src/components/views/auth/LoginWithQRFlow.tsx +++ b/src/components/views/auth/LoginWithQRFlow.tsx @@ -14,10 +14,17 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { createRef } from "react"; -import { MSC4108FailureReason, ClientRendezvousFailureReason } from "matrix-js-sdk/src/rendezvous"; +import React, { createRef, ReactNode } from "react"; +import { + ClientRendezvousFailureReason, + LegacyRendezvousFailureReason, + MSC4108FailureReason, +} from "matrix-js-sdk/src/rendezvous"; import { Icon as ChevronLeftIcon } from "@vector-im/compound-design-tokens/icons/chevron-left.svg"; +import { Icon as CheckCircleSolidIcon } from "@vector-im/compound-design-tokens/icons/check-circle-solid.svg"; +import { Icon as ErrorIcon } from "@vector-im/compound-design-tokens/icons/error.svg"; import { Heading, MFAInput, Text } from "@vector-im/compound-web"; +import classNames from "classnames"; import { _t } from "../../../languageHandler"; import AccessibleButton from "../elements/AccessibleButton"; @@ -87,71 +94,92 @@ export default class LoginWithQRFlow extends React.Component + {_t("auth|qr_code_login|error_insecure_channel_detected")} + + + {_t("auth|qr_code_login|error_insecure_channel_detected_instructions")} + +
      +
    1. {_t("auth|qr_code_login|error_insecure_channel_detected_instructions_1")}
    2. +
    3. {_t("auth|qr_code_login|error_insecure_channel_detected_instructions_2")}
    4. +
    5. {_t("auth|qr_code_login|error_insecure_channel_detected_instructions_3")}
    6. +
    + + ); break; - case MSC4108FailureReason.UserCancelled: - cancellationMessage = _t("auth|qr_code_login|error_request_cancelled"); + + case ClientRendezvousFailureReason.OtherDeviceAlreadySignedIn: + success = true; + title = _t("auth|qr_code_login|error_other_device_already_signed_in_title"); + message = _t("auth|qr_code_login|error_other_device_already_signed_in"); break; + case LoginWithQRFailureReason.RateLimited: - cancellationMessage = _t("auth|qr_code_login|error_rate_limited"); - break; - case ClientRendezvousFailureReason.Unknown: - cancellationMessage = _t("auth|qr_code_login|error_unexpected"); - break; - case ClientRendezvousFailureReason.HomeserverLacksSupport: - cancellationMessage = _t("auth|qr_code_login|error_homeserver_lacks_support"); - break; - case ClientRendezvousFailureReason.InsecureChannelDetected: - cancellationMessage = _t("auth|qr_code_login|error_insecure_connection"); - break; - case MSC4108FailureReason.UnexpectedMessageReceived: - cancellationMessage = _t("auth|qr_code_login|error_unexpected_message"); + title = _t("error|something_went_wrong"); + message = _t("auth|qr_code_login|error_rate_limited"); break; + case MSC4108FailureReason.DeviceAlreadyExists: - cancellationMessage = _t("auth|qr_code_login|error_device_already_exists"); - break; case MSC4108FailureReason.DeviceNotFound: - cancellationMessage = _t("auth|qr_code_login|error_device_not_found"); - break; + case MSC4108FailureReason.UnexpectedMessageReceived: + case ClientRendezvousFailureReason.Unknown: default: - cancellationMessage = _t("auth|qr_code_login|error_unexpected"); + title = _t("error|something_went_wrong"); + message = _t("auth|qr_code_login|error_unexpected"); break; } - centreTitle = true; + className = "mx_LoginWithQR_error"; backButton = false; - main =

    {cancellationMessage}

    ; - buttons = ( + main = ( <> - - {_t("action|try_again")} - - {this.cancelButton()} + {success ? : } +
+ + {title} + + {typeof message === "object" ? message :

{message}

} ); break; + } case Phase.LegacyConnected: backButton = false; main = ( @@ -169,13 +197,6 @@ export default class LoginWithQRFlow extends React.Component - - {_t("action|cancel")} - {_t("action|approve")} + + {_t("action|cancel")} + ); break; @@ -221,13 +249,6 @@ export default class LoginWithQRFlow extends React.Component - - {_t("action|cancel")} - {_t("action|continue")} + + {_t("action|cancel")} + ); break; @@ -291,30 +319,27 @@ export default class LoginWithQRFlow extends React.Component -
- {backButton ? ( -
- - - -
- {_t("settings|sessions|title")} / {_t("settings|sessions|sign_in_with_qr")} -
+
+ {backButton ? ( +
+ + + +
+ {_t("settings|sessions|title")} / {_t("settings|sessions|sign_in_with_qr")}
- ) : null} -
+
+ ) : null}
{main}
{buttons}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 5dfe46ab9b1..abc7666aa58 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -246,25 +246,28 @@ "phone_optional_label": "Phone (optional)", "qr_code_login": { "approve_access_warning": "By approving access for this device, it will have full access to your account.", - "check_code_explainer": "This will verify that the connection to your other device is secure.", - "check_code_heading": "Enter the number shown on your other device", + "check_code_explainer": "Confirm that you see a green checkmark on the other device to verify that the connection is secure.", + "check_code_heading": "Do you see a green checkmark on your other device?", "check_code_input_label": "2-digit code", "check_code_mismatch": "The numbers don't match", "completing_setup": "Completing set up of your new device", "confirm_code_match": "Check that the code below matches with your other device:", - "error_device_already_exists": "The specified device is already signed in.", - "error_device_already_signed_in": "The other device is already signed in.", - "error_device_not_found": "The new device does not appear to have signed in.", - "error_device_not_signed_in": "The other device isn't signed in.", - "error_device_unsupported": "Linking with this device is not supported.", - "error_homeserver_lacks_support": "The homeserver doesn't support signing in another device.", - "error_insecure_connection": "Connection not secure.", - "error_linking_incomplete": "The linking wasn't completed in the required time.", + "error_expired": "Sign in expired. Please try again.", + "error_expired_title": "The sign in was not completed in time", + "error_insecure_channel_detected": "A secure connection could not be made to the new device. Your existing devices are still safe and you don't need to worry about them.", + "error_insecure_channel_detected_instructions": "Now what?", + "error_insecure_channel_detected_instructions_1": "Try signing in to the other device again with a QR code in case this was a network problem", + "error_insecure_channel_detected_instructions_2": "If you encounter the same problem, try a different wifi network or use your mobile data instead of wifi", + "error_insecure_channel_detected_instructions_3": "If that doesn't work, sign in manually", + "error_insecure_channel_detected_title": "Connection not secure", + "error_other_device_already_signed_in": "You don’t need to do anything else.", + "error_other_device_already_signed_in_title": "Your other device is already signed in", "error_rate_limited": "Too many attempts in a short time. Wait some time before trying again.", - "error_request_cancelled": "The request was cancelled.", - "error_request_declined": "The request was declined.", - "error_unexpected": "An unexpected error occurred.", - "error_unexpected_message": "An unexpected message was received from the other device.", + "error_unexpected": "An unexpected error occurred. The request to connect your other device has been cancelled.", + "error_unsupported_protocol": "This device does not support signing in to the other device with a QR code.", + "error_unsupported_protocol_title": "Other device not compatible", + "error_user_cancelled": "The sign in was cancelled on the other device.", + "error_user_cancelled_title": "Sign in request cancelled", "follow_remaining_instructions": "Follow the remaining instructions to verify your other device", "open_element_other_device": "Open %(brand)s on your other device", "point_the_camera": "Point the camera at the QR code shown here", diff --git a/test/components/views/settings/devices/LoginWithQR-test.tsx b/test/components/views/settings/devices/LoginWithQR-test.tsx index 772245f608d..9b9e33ae87f 100644 --- a/test/components/views/settings/devices/LoginWithQR-test.tsx +++ b/test/components/views/settings/devices/LoginWithQR-test.tsx @@ -139,51 +139,6 @@ describe("", () => { expect(rendezvous.startAfterShowingCode).toHaveBeenCalled(); }); - test("render QR then cancel and try again", async () => { - const onFinished = jest.fn(); - jest.spyOn(MSC3906Rendezvous.prototype, "startAfterShowingCode").mockImplementation(() => unresolvedPromise()); - render(getComponent({ client, onFinished })); - const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0]; - - await waitFor(() => - expect(mockedFlow).toHaveBeenLastCalledWith( - expect.objectContaining({ - phase: Phase.ShowingQR, - }), - ), - ); - // display QR code - expect(mockedFlow).toHaveBeenLastCalledWith({ - phase: Phase.ShowingQR, - code: mockRendezvousCode, - onClick: expect.any(Function), - }); - expect(rendezvous.generateCode).toHaveBeenCalled(); - expect(rendezvous.startAfterShowingCode).toHaveBeenCalled(); - - // cancel - const onClick = mockedFlow.mock.calls[0][0].onClick; - await onClick(Click.Cancel); - expect(onFinished).toHaveBeenCalledWith(false); - expect(rendezvous.cancel).toHaveBeenCalledWith(LegacyRendezvousFailureReason.UserCancelled); - - // try again - onClick(Click.TryAgain); - await waitFor(() => - expect(mockedFlow).toHaveBeenLastCalledWith( - expect.objectContaining({ - phase: Phase.ShowingQR, - }), - ), - ); - // display QR code - expect(mockedFlow).toHaveBeenLastCalledWith({ - phase: Phase.ShowingQR, - code: mockRendezvousCode, - onClick: expect.any(Function), - }); - }); - test("render QR then back", async () => { const onFinished = jest.fn(); jest.spyOn(MSC3906Rendezvous.prototype, "startAfterShowingCode").mockReturnValue(unresolvedPromise()); diff --git a/test/components/views/settings/devices/LoginWithQRFlow-test.tsx b/test/components/views/settings/devices/LoginWithQRFlow-test.tsx index 5f0fa4cdff6..f381df3100b 100644 --- a/test/components/views/settings/devices/LoginWithQRFlow-test.tsx +++ b/test/components/views/settings/devices/LoginWithQRFlow-test.tsx @@ -113,8 +113,6 @@ describe("", () => { expect(screen.getAllByTestId("cancellation-message")).toHaveLength(1); expect(screen.getAllByTestId("try-again-button")).toHaveLength(1); expect(container).toMatchSnapshot(); - fireEvent.click(screen.getByTestId("try-again-button")); - expect(onClick).toHaveBeenCalledWith(Click.TryAgain); }); } }); From 4a8f76d3b834d28b2ae12e8163826fcf4ff8df33 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 18 Apr 2024 11:51:54 +0100 Subject: [PATCH 34/98] Split error for UserDeclined Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/auth/LoginWithQRFlow.tsx | 6 +++++- src/i18n/strings/en_EN.json | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/views/auth/LoginWithQRFlow.tsx b/src/components/views/auth/LoginWithQRFlow.tsx index 8baed9cf64c..1f80c33f73b 100644 --- a/src/components/views/auth/LoginWithQRFlow.tsx +++ b/src/components/views/auth/LoginWithQRFlow.tsx @@ -110,7 +110,6 @@ export default class LoginWithQRFlow extends React.Component Date: Thu, 18 Apr 2024 16:04:06 +0100 Subject: [PATCH 35/98] i18n Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/i18n/strings/en_EN.json | 56 ++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 6bdd5185391..13850cc20c6 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -246,40 +246,40 @@ "phone_optional_label": "Phone (optional)", "qr_code_login": { "approve_access_warning": "By approving access for this device, it will have full access to your account.", + "check_code_explainer": "Confirm that you see a green checkmark on the other device to verify that the connection is secure.", + "check_code_heading": "Do you see a green checkmark on your other device?", + "check_code_input_label": "2-digit code", + "check_code_mismatch": "The numbers don't match", "completing_setup": "Completing set up of your new device", "confirm_code_match": "Check that the code below matches with your other device:", + "error_expired": "Sign in expired. Please try again.", + "error_expired_title": "The sign in was not completed in time", + "error_insecure_channel_detected": "A secure connection could not be made to the new device. Your existing devices are still safe and you don't need to worry about them.", + "error_insecure_channel_detected_instructions": "Now what?", + "error_insecure_channel_detected_instructions_1": "Try signing in to the other device again with a QR code in case this was a network problem", + "error_insecure_channel_detected_instructions_2": "If you encounter the same problem, try a different wifi network or use your mobile data instead of wifi", + "error_insecure_channel_detected_instructions_3": "If that doesn't work, sign in manually", + "error_insecure_channel_detected_title": "Connection not secure", + "error_other_device_already_signed_in": "You don’t need to do anything else.", + "error_other_device_already_signed_in_title": "Your other device is already signed in", "error_rate_limited": "Too many attempts in a short time. Wait some time before trying again.", "error_unexpected": "An unexpected error occurred. The request to connect your other device has been cancelled.", + "error_unsupported_protocol": "This device does not support signing in to the other device with a QR code.", + "error_unsupported_protocol_title": "Other device not compatible", + "error_user_cancelled": "The sign in was cancelled on the other device.", + "error_user_cancelled_title": "Sign in request cancelled", + "error_user_declined": "You declined the request from your other device to sign in.", + "error_user_declined_title": "Sign in declined", "follow_remaining_instructions": "Follow the remaining instructions to verify your other device", "open_element_other_device": "Open %(brand)s on your other device", "point_the_camera": "Point the camera at the QR code shown here", "scan_code_instruction": "Scan the QR code with another device", "scan_qr_code": "Sign in with QR code", + "security_code": "Security code", + "security_code_prompt": "If asked, enter the code below on your other device.", "select_qr_code": "Select \"%(scanQRCode)s\"", "sign_in_new_device": "Sign in new device", - "waiting_for_device": "Waiting for device to sign in", - "security_code_prompt": "If asked, enter the code below on your other device.", - "security_code": "Security code", - "error_user_declined_title": "Sign in declined", - "error_user_declined": "You declined the request from your other device to sign in.", - "error_user_cancelled_title": "Sign in request cancelled", - "error_user_cancelled": "The sign in was cancelled on the other device.", - "error_unsupported_protocol_title": "Other device not compatible", - "error_unsupported_protocol": "This device does not support signing in to the other device with a QR code.", - "error_other_device_already_signed_in_title": "Your other device is already signed in", - "error_other_device_already_signed_in": "You don’t need to do anything else.", - "error_insecure_channel_detected_title": "Connection not secure", - "error_insecure_channel_detected_instructions_3": "If that doesn't work, sign in manually", - "error_insecure_channel_detected_instructions_2": "If you encounter the same problem, try a different wifi network or use your mobile data instead of wifi", - "error_insecure_channel_detected_instructions_1": "Try signing in to the other device again with a QR code in case this was a network problem", - "error_insecure_channel_detected_instructions": "Now what?", - "error_insecure_channel_detected": "A secure connection could not be made to the new device. Your existing devices are still safe and you don't need to worry about them.", - "error_expired_title": "The sign in was not completed in time", - "error_expired": "Sign in expired. Please try again.", - "check_code_mismatch": "The numbers don't match", - "check_code_input_label": "2-digit code", - "check_code_heading": "Do you see a green checkmark on your other device?", - "check_code_explainer": "Confirm that you see a green checkmark on the other device to verify that the connection is secure." + "waiting_for_device": "Waiting for device to sign in" }, "register_action": "Create Account", "registration": { @@ -3225,6 +3225,10 @@ }, "creation_summary_dm": "%(creator)s created this DM.", "creation_summary_room": "%(creator)s created and configured the room.", + "decryption_failure": { + "blocked": "The sender has blocked you from receiving this message", + "unable_to_decrypt": "Unable to decrypt message" + }, "disambiguated_profile": "%(displayName)s (%(matrixId)s)", "download_action_decrypting": "Decrypting", "download_action_downloading": "Downloading", @@ -3656,10 +3660,6 @@ "one": "Show %(count)s other preview", "other": "Show %(count)s other previews" } - }, - "decryption_failure": { - "blocked": "The sender has blocked you from receiving this message", - "unable_to_decrypt": "Unable to decrypt message" } }, "truncated_list_n_more": { @@ -4081,4 +4081,4 @@ "wordByItself": "A word by itself is easy to guess" } } -} \ No newline at end of file +} From f0c88a25c5bb7993e96388ea5d3e377b8167b004 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 18 Apr 2024 16:05:16 +0100 Subject: [PATCH 36/98] Fudge yarn.lock Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- yarn.lock | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/yarn.lock b/yarn.lock index f89fd40bf9c..418a2f5c844 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1873,8 +1873,7 @@ "@matrix-org/matrix-sdk-crypto-wasm@^4.9.0": version "4.9.0" - resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-4.9.0.tgz#9dfed83e33f760650596c4e5c520e5e4c53355d2" - integrity sha512-/bgA4QfE7qkK6GFr9hnhjAvRSebGrmEJxukU0ukbudZcYvbzymoBBM8j3HeULXZT8kbw8WH6z63txYTMCBSDOA== + resolved "https://codeload.github.com/matrix-org/matrix-rust-sdk-crypto-wasm/tar.gz/78c5fb5cc29979d0bd188a8b0ec163dd30aa7936" "@matrix-org/matrix-wysiwyg@2.17.0": version "2.17.0" From 72fe90df28dea019370dfb672826b49014131089 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 18 Apr 2024 16:09:27 +0100 Subject: [PATCH 37/98] Copy prepare hack Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 72cd0fee50e..d098e8e27a6 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,8 @@ "test:playwright:screenshots:build": "docker build playwright -t matrix-react-sdk-playwright", "test:playwright:screenshots:run": "docker run --rm --network host -v $(pwd)/../:/work/ -v /var/run/docker.sock:/var/run/docker.sock -v /tmp/:/tmp/ -it matrix-react-sdk-playwright", "coverage": "yarn test --coverage", - "lint:workflows": "find .github/workflows -type f \\( -iname '*.yaml' -o -iname '*.yml' \\) | xargs -I {} sh -c 'echo \"Linting {}\"; action-validator \"{}\"'" + "lint:workflows": "find .github/workflows -type f \\( -iname '*.yaml' -o -iname '*.yml' \\) | xargs -I {} sh -c 'echo \"Linting {}\"; action-validator \"{}\"'", + "prepare": "cd node_modules/@matrix-org/matrix-sdk-crypto-wasm && npm install && npm run prepare" }, "resolutions": { "@types/react-dom": "17.0.21", From 83935e71b9b18e41730e2f06a8631aae37f29519 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 18 Apr 2024 16:18:39 +0100 Subject: [PATCH 38/98] Try s'more Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index d098e8e27a6..8b1a11c7db2 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "@babel/runtime": "^7.12.5", "@matrix-org/analytics-events": "^0.19.0", "@matrix-org/emojibase-bindings": "^1.1.2", + "@matrix-org/matrix-sdk-crypto-wasm": "github:matrix-org/matrix-rust-sdk-crypto-wasm#poljar/qr-login", "@matrix-org/matrix-wysiwyg": "2.17.0", "@matrix-org/olm": "3.2.15", "@matrix-org/react-sdk-module-api": "^2.4.0", From e86d0c384b5b85240a2123305dccb72bfd47b545 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 18 Apr 2024 16:23:01 +0100 Subject: [PATCH 39/98] Try s'more Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- yarn.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index 418a2f5c844..2ca554431b0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1871,7 +1871,7 @@ emojibase "^15.0.0" emojibase-data "^15.0.0" -"@matrix-org/matrix-sdk-crypto-wasm@^4.9.0": +"@matrix-org/matrix-sdk-crypto-wasm@^4.9.0", "@matrix-org/matrix-sdk-crypto-wasm@github:matrix-org/matrix-rust-sdk-crypto-wasm#poljar/qr-login": version "4.9.0" resolved "https://codeload.github.com/matrix-org/matrix-rust-sdk-crypto-wasm/tar.gz/78c5fb5cc29979d0bd188a8b0ec163dd30aa7936" From 932b2fc45361bde70de75ccdade4ed337515da8c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 18 Apr 2024 17:17:41 +0100 Subject: [PATCH 40/98] Try s'more Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 8b1a11c7db2..953bd553545 100644 --- a/package.json +++ b/package.json @@ -64,13 +64,13 @@ "@types/react-dom": "17.0.21", "@types/react": "17.0.68", "oidc-client-ts": "3.0.1", - "jwt-decode": "4.0.0" + "jwt-decode": "4.0.0", + "@matrix-org/matrix-sdk-crypto-wasm": "github:matrix-org/matrix-rust-sdk-crypto-wasm#poljar/qr-login" }, "dependencies": { "@babel/runtime": "^7.12.5", "@matrix-org/analytics-events": "^0.19.0", "@matrix-org/emojibase-bindings": "^1.1.2", - "@matrix-org/matrix-sdk-crypto-wasm": "github:matrix-org/matrix-rust-sdk-crypto-wasm#poljar/qr-login", "@matrix-org/matrix-wysiwyg": "2.17.0", "@matrix-org/olm": "3.2.15", "@matrix-org/react-sdk-module-api": "^2.4.0", From 8bec40fa3aab1152b0782419209c7c2cb417540f Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 18 Apr 2024 17:33:00 +0100 Subject: [PATCH 41/98] Try s'more Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- package.json | 5 ++--- yarn.lock | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 953bd553545..bf5be373393 100644 --- a/package.json +++ b/package.json @@ -58,14 +58,13 @@ "test:playwright:screenshots:run": "docker run --rm --network host -v $(pwd)/../:/work/ -v /var/run/docker.sock:/var/run/docker.sock -v /tmp/:/tmp/ -it matrix-react-sdk-playwright", "coverage": "yarn test --coverage", "lint:workflows": "find .github/workflows -type f \\( -iname '*.yaml' -o -iname '*.yml' \\) | xargs -I {} sh -c 'echo \"Linting {}\"; action-validator \"{}\"'", - "prepare": "cd node_modules/@matrix-org/matrix-sdk-crypto-wasm && npm install && npm run prepare" + "prepare": "cd node_modules/matrix-js-sdk/node_modules/@matrix-org/matrix-sdk-crypto-wasm && npm install && npm run prepare" }, "resolutions": { "@types/react-dom": "17.0.21", "@types/react": "17.0.68", "oidc-client-ts": "3.0.1", - "jwt-decode": "4.0.0", - "@matrix-org/matrix-sdk-crypto-wasm": "github:matrix-org/matrix-rust-sdk-crypto-wasm#poljar/qr-login" + "jwt-decode": "4.0.0" }, "dependencies": { "@babel/runtime": "^7.12.5", diff --git a/yarn.lock b/yarn.lock index 2ca554431b0..418a2f5c844 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1871,7 +1871,7 @@ emojibase "^15.0.0" emojibase-data "^15.0.0" -"@matrix-org/matrix-sdk-crypto-wasm@^4.9.0", "@matrix-org/matrix-sdk-crypto-wasm@github:matrix-org/matrix-rust-sdk-crypto-wasm#poljar/qr-login": +"@matrix-org/matrix-sdk-crypto-wasm@^4.9.0": version "4.9.0" resolved "https://codeload.github.com/matrix-org/matrix-rust-sdk-crypto-wasm/tar.gz/78c5fb5cc29979d0bd188a8b0ec163dd30aa7936" From fbcac6cc3f37588c8c8db8cac0c12f0a54887f34 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 18 Apr 2024 17:35:42 +0100 Subject: [PATCH 42/98] Try s'more Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .github/workflows/static_analysis.yaml | 2 +- .github/workflows/tests.yml | 2 +- package.json | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/static_analysis.yaml b/.github/workflows/static_analysis.yaml index 070ac5f8544..6e225467afa 100644 --- a/.github/workflows/static_analysis.yaml +++ b/.github/workflows/static_analysis.yaml @@ -27,7 +27,7 @@ jobs: cache: "yarn" - name: Install Deps - run: "./scripts/ci/install-deps.sh --ignore-scripts" + run: "./scripts/ci/install-deps.sh" - name: Typecheck run: "yarn run lint:types" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7089569f73e..3815c4fb4cc 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -47,7 +47,7 @@ jobs: cache: "yarn" - name: Install Deps - run: "./scripts/ci/install-deps.sh --ignore-scripts" + run: "./scripts/ci/install-deps.sh" env: JS_SDK_GITHUB_BASE_REF: ${{ inputs.matrix-js-sdk-sha }} diff --git a/package.json b/package.json index bf5be373393..72cd0fee50e 100644 --- a/package.json +++ b/package.json @@ -57,8 +57,7 @@ "test:playwright:screenshots:build": "docker build playwright -t matrix-react-sdk-playwright", "test:playwright:screenshots:run": "docker run --rm --network host -v $(pwd)/../:/work/ -v /var/run/docker.sock:/var/run/docker.sock -v /tmp/:/tmp/ -it matrix-react-sdk-playwright", "coverage": "yarn test --coverage", - "lint:workflows": "find .github/workflows -type f \\( -iname '*.yaml' -o -iname '*.yml' \\) | xargs -I {} sh -c 'echo \"Linting {}\"; action-validator \"{}\"'", - "prepare": "cd node_modules/matrix-js-sdk/node_modules/@matrix-org/matrix-sdk-crypto-wasm && npm install && npm run prepare" + "lint:workflows": "find .github/workflows -type f \\( -iname '*.yaml' -o -iname '*.yml' \\) | xargs -I {} sh -c 'echo \"Linting {}\"; action-validator \"{}\"'" }, "resolutions": { "@types/react-dom": "17.0.21", From e5b9c5d0c01ef48f77cf2c557e90767c988efa9d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 18 Apr 2024 17:49:15 +0100 Subject: [PATCH 43/98] Update snapshots Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../LoginWithQRFlow-test.tsx.snap | 431 ++++-------------- 1 file changed, 79 insertions(+), 352 deletions(-) diff --git a/test/components/views/settings/devices/__snapshots__/LoginWithQRFlow-test.tsx.snap b/test/components/views/settings/devices/__snapshots__/LoginWithQRFlow-test.tsx.snap index 1f6c51d99e1..11b6406744e 100644 --- a/test/components/views/settings/devices/__snapshots__/LoginWithQRFlow-test.tsx.snap +++ b/test/components/views/settings/devices/__snapshots__/LoginWithQRFlow-test.tsx.snap @@ -1,47 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[` errors renders data_mismatch 1`] = ` -
-
-
-
-

- The request was cancelled. -

-
-
-
- Try again -
-
- Cancel -
-
-
-
-`; - exports[` errors renders expired 1`] = `
errors renders homeserver_lacks_support 1`] = `
`; -exports[` errors renders invalid_code 1`] = ` -
-
-
-
-

- The scanned code is invalid. -

-
-
-
- Try again -
-
- Cancel -
-
-
-
-`; - -exports[` errors renders other_device_already_signed_in 1`] = ` -
-
-
-
-

- The other device is already signed in. -

-
-
-
- Try again -
-
- Cancel -
-
-
-
-`; - -exports[` errors renders other_device_not_signed_in 1`] = ` -
-
-
-
-

- The other device isn't signed in. -

-
-
-
- Try again -
-
- Cancel -
-
-
-
-`; - exports[` errors renders rate_limited 1`] = `
errors renders rate_limited 1`] = `
`; -exports[` errors renders unexpected_message 1`] = ` -
-
-
-
-

- The request was cancelled. -

-
-
-
- Try again -
-
- Cancel -
-
-
-
-`; - exports[` errors renders unknown 1`] = `
errors renders unsupported_algorithm 1`] = `
`; -exports[` errors renders unsupported_transport 1`] = ` +exports[` errors renders user_cancelled 1`] = `
errors renders unsupported_transport 1`] = `
`; -exports[` errors renders user_cancelled 1`] = ` +exports[` errors renders user_declined 1`] = `
errors renders user_cancelled 1`] = `

- The request was cancelled. + The request was declined on the other device.

errors renders user_cancelled 1`] = `
`; -exports[` errors renders user_declined 1`] = ` +exports[` renders QR code 1`] = `
-
-

- The request was declined on the other device. -

-
-
- Try again +
- Cancel -
-
-
-
-`; - -exports[` renders QR code 1`] = ` -
-
-
-
-
-
-
-
- Sessions - / - Link new device -
+ Sessions + / + Link new device
-

+

Scan the QR code with another device

renders QR code 1`] = ` Select " - Scan QR code + Sign in with QR code " @@ -629,9 +375,6 @@ exports[` renders code when connected 1`] = ` class="mx_LoginWithQR" data-testid="login-with-qr" > -
@@ -658,20 +401,20 @@ exports[` renders code when connected 1`] = ` class="mx_LoginWithQR_buttons" >
- Cancel + Approve
- Approve + Cancel
@@ -755,27 +498,23 @@ exports[` renders spinner while loading 1`] = ` data-testid="login-with-qr" >
-
-
-
-
- Sessions - / - Link new device -
+
+
+
+ Sessions + / + Link new device
renders spinner while signing in 1`] = ` data-testid="login-with-qr" >
-
-
-
-
- Sessions - / - Link new device -
+
+
+
+ Sessions + / + Link new device
renders spinner while verifying 1`] = ` data-testid="login-with-qr" >
-
-
-
-
- Sessions - / - Link new device -
+
+
+
+ Sessions + / + Link new device
renders spinner whilst QR generating 1`] = ` data-testid="login-with-qr" >
-
-
-
-
- Sessions - / - Link new device -
+
+
+
+ Sessions + / + Link new device
Date: Thu, 18 Apr 2024 18:03:49 +0100 Subject: [PATCH 44/98] Fix tests Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/auth/LoginWithQRFlow.tsx | 4 + src/i18n/strings/en_EN.json | 1 + .../settings/devices/LoginWithQRFlow-test.tsx | 11 +- .../LoginWithQRFlow-test.tsx.snap | 494 +++++++++++++----- .../tabs/user/SessionManagerTab-test.tsx | 2 +- 5 files changed, 364 insertions(+), 148 deletions(-) diff --git a/src/components/views/auth/LoginWithQRFlow.tsx b/src/components/views/auth/LoginWithQRFlow.tsx index 1f80c33f73b..dec4b232428 100644 --- a/src/components/views/auth/LoginWithQRFlow.tsx +++ b/src/components/views/auth/LoginWithQRFlow.tsx @@ -307,6 +307,10 @@ export default class LoginWithQRFlow extends React.Component diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 13850cc20c6..68a759bed77 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -252,6 +252,7 @@ "check_code_mismatch": "The numbers don't match", "completing_setup": "Completing set up of your new device", "confirm_code_match": "Check that the code below matches with your other device:", + "connecting": "Connecting…", "error_expired": "Sign in expired. Please try again.", "error_expired_title": "The sign in was not completed in time", "error_insecure_channel_detected": "A secure connection could not be made to the new device. Your existing devices are still safe and you don't need to worry about them.", diff --git a/test/components/views/settings/devices/LoginWithQRFlow-test.tsx b/test/components/views/settings/devices/LoginWithQRFlow-test.tsx index f381df3100b..1262507afa8 100644 --- a/test/components/views/settings/devices/LoginWithQRFlow-test.tsx +++ b/test/components/views/settings/devices/LoginWithQRFlow-test.tsx @@ -54,7 +54,7 @@ describe("", () => { expect(screen.getAllByTestId("cancel-button")).toHaveLength(1); expect(container).toMatchSnapshot(); fireEvent.click(screen.getByTestId("cancel-button")); - expect(onClick).toHaveBeenCalledWith(Click.Cancel); + expect(onClick).toHaveBeenCalledWith(Click.Cancel, undefined); }); it("renders QR code", async () => { @@ -69,7 +69,7 @@ describe("", () => { expect(screen.getAllByTestId("cancel-button")).toHaveLength(1); expect(container).toMatchSnapshot(); fireEvent.click(screen.getByTestId("cancel-button")); - expect(onClick).toHaveBeenCalledWith(Click.Cancel); + expect(onClick).toHaveBeenCalledWith(Click.Cancel, undefined); }); it("renders code when connected", async () => { @@ -79,9 +79,9 @@ describe("", () => { expect(screen.getAllByTestId("approve-login-button")).toHaveLength(1); expect(container).toMatchSnapshot(); fireEvent.click(screen.getByTestId("decline-login-button")); - expect(onClick).toHaveBeenCalledWith(Click.Decline); + expect(onClick).toHaveBeenCalledWith(Click.Decline, undefined); fireEvent.click(screen.getByTestId("approve-login-button")); - expect(onClick).toHaveBeenCalledWith(Click.Approve); + expect(onClick).toHaveBeenCalledWith(Click.Approve, undefined); }); it("renders spinner while signing in", async () => { @@ -89,7 +89,7 @@ describe("", () => { expect(screen.getAllByTestId("cancel-button")).toHaveLength(1); expect(container).toMatchSnapshot(); fireEvent.click(screen.getByTestId("cancel-button")); - expect(onClick).toHaveBeenCalledWith(Click.Cancel); + expect(onClick).toHaveBeenCalledWith(Click.Cancel, undefined); }); it("renders spinner while verifying", async () => { @@ -111,7 +111,6 @@ describe("", () => { }), ); expect(screen.getAllByTestId("cancellation-message")).toHaveLength(1); - expect(screen.getAllByTestId("try-again-button")).toHaveLength(1); expect(container).toMatchSnapshot(); }); } diff --git a/test/components/views/settings/devices/__snapshots__/LoginWithQRFlow-test.tsx.snap b/test/components/views/settings/devices/__snapshots__/LoginWithQRFlow-test.tsx.snap index 11b6406744e..75d91ee40e9 100644 --- a/test/components/views/settings/devices/__snapshots__/LoginWithQRFlow-test.tsx.snap +++ b/test/components/views/settings/devices/__snapshots__/LoginWithQRFlow-test.tsx.snap @@ -1,169 +1,307 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[` errors renders expired 1`] = ` +exports[` errors renders authorization_expired 1`] = `
-
+
+
+
+

+ The sign in was not completed in time +

- The linking wasn't completed in the required time. + Sign in expired. Please try again.

+
+
+`; + +exports[` errors renders check_code_mismatch 1`] = ` +
+
+
- Try again +
-
- Cancel -
+ Something went wrong! + +

+ An unexpected error occurred. The request to connect your other device has been cancelled. +

+
`; -exports[` errors renders homeserver_lacks_support 1`] = ` +exports[` errors renders device_already_exists 1`] = `
-
+
+
+
+

+ Something went wrong! +

- The homeserver doesn't support signing in another device. + An unexpected error occurred. The request to connect your other device has been cancelled.

+
+
+`; + +exports[` errors renders device_not_found 1`] = ` +
+
+
- Try again +
-
- Cancel -
+ Something went wrong! + +

+ An unexpected error occurred. The request to connect your other device has been cancelled. +

+
`; -exports[` errors renders rate_limited 1`] = ` +exports[` errors renders expired 1`] = `
-
+
+
+
+

+ The sign in was not completed in time +

- Too many attempts in a short time. Wait some time before trying again. + Sign in expired. Please try again.

+
+
+`; + +exports[` errors renders homeserver_lacks_support 1`] = ` +
+
+
- Try again +
-
- Cancel -
+ Something went wrong! + +

+ An unexpected error occurred. The request to connect your other device has been cancelled. +

+
`; -exports[` errors renders unknown 1`] = ` +exports[` errors renders rate_limited 1`] = `
-
+
+
+
+

+ Something went wrong! +

- An unexpected error occurred. + Too many attempts in a short time. Wait some time before trying again.

+
+
+`; + +exports[` errors renders unexpected_message_received 1`] = ` +
+
+
- Try again +
+

+ Something went wrong! +

+

+ An unexpected error occurred. The request to connect your other device has been cancelled. +

+
+
+
+
+`; + +exports[` errors renders unknown 1`] = ` +
+
+
- Cancel +
+

+ Something went wrong! +

+

+ An unexpected error occurred. The request to connect your other device has been cancelled. +

+
`; @@ -171,125 +309,203 @@ exports[` errors renders unknown 1`] = ` exports[` errors renders unsupported_algorithm 1`] = `
-
+
+
+
+

+ Something went wrong! +

- Linking with this device is not supported. + An unexpected error occurred. The request to connect your other device has been cancelled.

+
+
+`; + +exports[` errors renders unsupported_protocol 1`] = ` +
+
+
- Try again +
-
- Cancel -
+ Other device not compatible + +

+ This device does not support signing in to the other device with a QR code. +

+
`; -exports[` errors renders user_cancelled 1`] = ` +exports[` errors renders unsupported_protocol 2`] = `
-
+
+
+
+

+ Other device not compatible +

- The request was cancelled. + This device does not support signing in to the other device with a QR code.

+
+
+`; + +exports[` errors renders user_cancelled 1`] = ` +
+
+
- Try again +
-
- Cancel -
+ Sign in request cancelled + +

+ The sign in was cancelled on the other device. +

+
`; -exports[` errors renders user_declined 1`] = ` +exports[` errors renders user_cancelled 2`] = `
-
+
+
+
+

+ Sign in request cancelled +

- The request was declined on the other device. + The sign in was cancelled on the other device.

+
+
+`; + +exports[` errors renders user_declined 1`] = ` +
+
+
- Try again +
-
- Cancel -
+ Sign in declined + +

+ You declined the request from your other device to sign in. +

+
`; @@ -428,27 +644,23 @@ exports[` renders spinner while connecting 1`] = ` data-testid="login-with-qr" >
-
-
-
-
- Sessions - / - Link new device -
+
+
+
+ Sessions + / + Link new device
", () => { const openIdConfiguration = mockOpenIdConfiguration(issuer); beforeEach(() => { - settingsValueSpy.mockClear().mockReturnValue(false); + settingsValueSpy.mockClear().mockReturnValue(true); // enable server support for qr login mockClient.getVersions.mockResolvedValue({ versions: [], From f694f6d1d357cfba4343c24b1e679d33a4cd5f7a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 18 Apr 2024 22:51:15 +0100 Subject: [PATCH 45/98] Fix tests Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/auth/LoginWithQR.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index 32a03977d19..fe4196187fd 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -245,14 +245,13 @@ export default class LoginWithQR extends React.Component { // we ask the user to confirm that the channel is secure } catch (e: RendezvousError | unknown) { logger.error("Error whilst approving login", e); - if (this.state.rendezvous instanceof MSC3906Rendezvous) { - await this.state.rendezvous?.cancel( - e instanceof RendezvousError - ? (e.code as LegacyRendezvousFailureReason) - : LegacyRendezvousFailureReason.Unknown, - ); + if (rendezvous instanceof MSC3906Rendezvous) { + // only set to error phase if it hasn't already been set by onFailure or similar + if (this.state.phase !== Phase.Error) { + this.setState({ phase: Phase.Error, failureReason: LegacyRendezvousFailureReason.Unknown }); + } } else { - await this.state.rendezvous?.cancel( + await rendezvous?.cancel( e instanceof RendezvousError ? (e.code as MSC4108FailureReason) : ClientRendezvousFailureReason.Unknown, From 96b6a052101acc9c8a85f4d6e0c885eadd8c73cb Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 19 Apr 2024 09:07:34 +0100 Subject: [PATCH 46/98] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- res/css/_common.pcss | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/res/css/_common.pcss b/res/css/_common.pcss index c5a45886b3f..3cc912cae24 100644 --- a/res/css/_common.pcss +++ b/res/css/_common.pcss @@ -177,9 +177,9 @@ a:visited { color: $accent-alt; } -:not(.mx_no_textinput) > input[type="text"], -:not(.mx_no_textinput) > input[type="search"], -:not(.mx_no_textinput) > input[type="password"] { +:not(.mx_textinput):not(.mx_Field):not(.mx_no_textinput) > input[type="text"], +:not(.mx_textinput):not(.mx_Field):not(.mx_no_textinput) > input[type="search"], +:not(.mx_textinput):not(.mx_Field):not(.mx_no_textinput) > input[type="password"] { padding: 9px; font: var(--cpd-font-body-md-semibold); font-weight: var(--cpd-font-weight-semibold); From 0ce0237e108e57453ed63743ccdb4f6cb52ab788 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 19 Apr 2024 09:23:03 +0100 Subject: [PATCH 47/98] Fix styling Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- res/css/views/auth/_LoginWithQR.pcss | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/res/css/views/auth/_LoginWithQR.pcss b/res/css/views/auth/_LoginWithQR.pcss index 5fb35a86a17..2d86029364f 100644 --- a/res/css/views/auth/_LoginWithQR.pcss +++ b/res/css/views/auth/_LoginWithQR.pcss @@ -159,13 +159,9 @@ limitations under the License. } } - .mx_LoginWithQR_checkCode { - margin-top: 4px; - height: 24px; - } - .mx_LoginWithQR_checkCode_input { margin-bottom: 4px; + text-align: unset; } .mx_LoginWithQR_heading { From 5e3f6e80c98963f3637b6faadf417189e336762e Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 19 Apr 2024 10:05:13 +0100 Subject: [PATCH 48/98] Fix styling Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- res/css/_common.pcss | 6 +++--- res/css/views/auth/_LoginWithQR.pcss | 9 ++++++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/res/css/_common.pcss b/res/css/_common.pcss index 3cc912cae24..20ed9dfa392 100644 --- a/res/css/_common.pcss +++ b/res/css/_common.pcss @@ -177,9 +177,9 @@ a:visited { color: $accent-alt; } -:not(.mx_textinput):not(.mx_Field):not(.mx_no_textinput) > input[type="text"], -:not(.mx_textinput):not(.mx_Field):not(.mx_no_textinput) > input[type="search"], -:not(.mx_textinput):not(.mx_Field):not(.mx_no_textinput) > input[type="password"] { +input[type="text"], +input[type="search"], +input[type="password"] { padding: 9px; font: var(--cpd-font-body-md-semibold); font-weight: var(--cpd-font-weight-semibold); diff --git a/res/css/views/auth/_LoginWithQR.pcss b/res/css/views/auth/_LoginWithQR.pcss index 2d86029364f..0c79f08071f 100644 --- a/res/css/views/auth/_LoginWithQR.pcss +++ b/res/css/views/auth/_LoginWithQR.pcss @@ -114,6 +114,7 @@ limitations under the License. color: 1px solid $input-placeholder; margin-bottom: 16px; line-height: 20px; + text-align: initial; } /* Circled number list item marker */ @@ -161,7 +162,13 @@ limitations under the License. .mx_LoginWithQR_checkCode_input { margin-bottom: 4px; - text-align: unset; + text-align: initial; + + input { + /* Workaround for one of the input rules in _common.pcss being not specific enough */ + padding: 0; + padding-inline-start: calc(40px / 2 - (1ch / 2)); + } } .mx_LoginWithQR_heading { From 2c767cce4b71f0153da3ed1b7e1bb9d87b6ab78c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 22 Apr 2024 09:09:10 +0100 Subject: [PATCH 49/98] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../settings/devices/LoginWithQRSection.tsx | 94 +++++++++---------- 1 file changed, 45 insertions(+), 49 deletions(-) diff --git a/src/components/views/settings/devices/LoginWithQRSection.tsx b/src/components/views/settings/devices/LoginWithQRSection.tsx index f1c297736a3..21e4a910d92 100644 --- a/src/components/views/settings/devices/LoginWithQRSection.tsx +++ b/src/components/views/settings/devices/LoginWithQRSection.tsx @@ -31,6 +31,7 @@ import AccessibleButton from "../../elements/AccessibleButton"; import SettingsSubsection from "../shared/SettingsSubsection"; import SettingsStore from "../../../../settings/SettingsStore"; import { Features } from "../../../../settings/Settings"; +import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext"; interface IProps { onShowQr: () => void; @@ -40,57 +41,52 @@ interface IProps { oidcClientConfig?: OidcClientConfig; } -export default class LoginWithQRSection extends React.Component { - public constructor(props: IProps) { - super(props); - } - - public render(): JSX.Element | null { - // Needs server support for (get_login_token or OIDC Device Authorization Grant) and MSC3886: - // in r0 of MSC3882 it is exposed as a feature flag, but in stable and unstable r1 it is a capability - const loginTokenCapability = GET_LOGIN_TOKEN_CAPABILITY.findIn( - this.props.capabilities, - ); - const getLoginTokenSupported = - !!this.props.versions?.unstable_features?.["org.matrix.msc3882"] || !!loginTokenCapability?.enabled; - const msc3886Supported = - !!this.props.versions?.unstable_features?.["org.matrix.msc3886"] || - !!this.props.wellKnown?.["io.element.rendezvous"]?.server; - const msc4108Supported = - !!this.props.versions?.unstable_features?.["org.matrix.msc4108"] || - !!this.props.wellKnown?.["io.element.rendezvous"]?.server; +const LoginWithQRSection: React.FC = ({ onShowQr, versions, capabilities, wellKnown, oidcClientConfig }) => { + const cli = useMatrixClientContext(); - const deviceAuthorizationGrantSupported = this.props.oidcClientConfig?.metadata?.grant_types_supported.includes( - "urn:ietf:params:oauth:grant-type:device_code", - ); + // Needs server support for (get_login_token or OIDC Device Authorization Grant) and MSC3886: + // in r0 of MSC3882 it is exposed as a feature flag, but in stable and unstable r1 it is a capability + const loginTokenCapability = GET_LOGIN_TOKEN_CAPABILITY.findIn(capabilities); + const getLoginTokenSupported = + !!versions?.unstable_features?.["org.matrix.msc3882"] || !!loginTokenCapability?.enabled; + const msc3886Supported = + !!versions?.unstable_features?.["org.matrix.msc3886"] || !!wellKnown?.["io.element.rendezvous"]?.server; + const msc4108Supported = + !!versions?.unstable_features?.["org.matrix.msc4108"] || !!wellKnown?.["io.element.rendezvous"]?.server; - logger.debug({ - msc3886Supported, - getLoginTokenSupported, - msc4108Supported, - deviceAuthorizationGrantSupported, - }); - const offerShowQr = this.props.oidcClientConfig - ? deviceAuthorizationGrantSupported && msc4108Supported && SettingsStore.getValue(Features.OidcNativeFlow) - : getLoginTokenSupported && msc3886Supported; + const deviceAuthorizationGrantSupported = oidcClientConfig?.metadata?.grant_types_supported.includes( + "urn:ietf:params:oauth:grant-type:device_code", + ); - // don't show anything if no method is available - if (!offerShowQr) { - return null; - } + logger.debug({ + msc3886Supported, + getLoginTokenSupported, + msc4108Supported, + deviceAuthorizationGrantSupported, + }); + const offerShowQr = oidcClientConfig + ? deviceAuthorizationGrantSupported && + msc4108Supported && + SettingsStore.getValue(Features.OidcNativeFlow) && + cli.getCrypto()?.supportsSecretsForQrLogin() + : getLoginTokenSupported && msc3886Supported; - return ( - -
-

- {_t("settings|sessions|sign_in_with_qr_description")} -

- - - {_t("settings|sessions|sign_in_with_qr_button")} - -
-
- ); + // don't show anything if no method is available + if (!offerShowQr) { + return null; } -} + + return ( + +
+

{_t("settings|sessions|sign_in_with_qr_description")}

+ + + {_t("settings|sessions|sign_in_with_qr_button")} + +
+
+ ); +}; + +export default LoginWithQRSection; From 2e1669501381e637d58bea41700e92e4b2999034 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 22 Apr 2024 09:09:46 +0100 Subject: [PATCH 50/98] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/MatrixClientPeg.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index 71b554f8fb1..caba704229b 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -338,7 +338,7 @@ class MatrixClientPegClass implements IMatrixClientPeg { await this.matrixClient.initRustCrypto(); if (secrets) { - this.matrixClient.getCrypto()?.importSecretsForQRLogin(secrets); + this.matrixClient.getCrypto()?.importSecretsForQrLogin(secrets); if (secrets.backup) { const backupInfo = await this.matrixClient.getKeyBackupVersion(); if (backupInfo) { From f9b388e7408858be44336e4ea8f84003954dd4c7 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 22 Apr 2024 15:59:55 +0100 Subject: [PATCH 51/98] Fix tests Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../views/settings/tabs/user/SessionManagerTab-test.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx b/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx index 7d750fb88f6..000dbf9bcf8 100644 --- a/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx +++ b/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx @@ -57,6 +57,7 @@ import { getClientInformationEventType } from "../../../../../../src/utils/devic import { SDKContext, SdkContextClass } from "../../../../../../src/contexts/SDKContext"; import { OidcClientStore } from "../../../../../../src/stores/oidc/OidcClientStore"; import { mockOpenIdConfiguration } from "../../../../../test-utils/oidc"; +import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext"; mockPlatformPeg(); @@ -123,6 +124,7 @@ describe("", () => { getDeviceVerificationStatus: jest.fn(), getUserDeviceInfo: jest.fn(), requestDeviceVerification: jest.fn().mockResolvedValue(mockVerificationRequest), + supportsSecretsForQrLogin: jest.fn().mockReturnValue(false), } as unknown as CryptoApi); let mockClient!: MockedObject; @@ -131,7 +133,9 @@ describe("", () => { const defaultProps = {}; const getComponent = (props = {}): React.ReactElement => ( - + + + ); @@ -1730,6 +1734,7 @@ describe("", () => { }, }); mockClient.getAuthIssuer.mockResolvedValue({ issuer }); + mockCrypto.supportsSecretsForQrLogin.mockReturnValue(true); fetchMock.mock(`${issuer}/.well-known/openid-configuration`, { ...openIdConfiguration, grant_types_supported: [ From 7542768a1bcc50a2c6dcd63cf6417c87b5f2acdc Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 23 Apr 2024 14:10:26 +0100 Subject: [PATCH 52/98] Require cross-signing to be ready for oidc-qr login Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/settings/devices/LoginWithQRSection.tsx | 3 ++- .../views/settings/tabs/user/SessionManagerTab-test.tsx | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/views/settings/devices/LoginWithQRSection.tsx b/src/components/views/settings/devices/LoginWithQRSection.tsx index 21e4a910d92..8e3f4cc961b 100644 --- a/src/components/views/settings/devices/LoginWithQRSection.tsx +++ b/src/components/views/settings/devices/LoginWithQRSection.tsx @@ -68,7 +68,8 @@ const LoginWithQRSection: React.FC = ({ onShowQr, versions, capabilities ? deviceAuthorizationGrantSupported && msc4108Supported && SettingsStore.getValue(Features.OidcNativeFlow) && - cli.getCrypto()?.supportsSecretsForQrLogin() + cli.getCrypto()?.supportsSecretsForQrLogin() && + cli.getCrypto()?.isCrossSigningReady() : getLoginTokenSupported && msc3886Supported; // don't show anything if no method is available diff --git a/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx b/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx index 000dbf9bcf8..d456b828413 100644 --- a/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx +++ b/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx @@ -125,6 +125,7 @@ describe("", () => { getUserDeviceInfo: jest.fn(), requestDeviceVerification: jest.fn().mockResolvedValue(mockVerificationRequest), supportsSecretsForQrLogin: jest.fn().mockReturnValue(false), + isCrossSigningReady: jest.fn().mockReturnValue(true), } as unknown as CryptoApi); let mockClient!: MockedObject; From 04fd1533774f339dbf3df14e15dbc180b698e4e7 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 23 Apr 2024 15:07:22 +0100 Subject: [PATCH 53/98] Handle etag missing state Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/auth/LoginWithQR.tsx | 1 + src/components/views/auth/LoginWithQRFlow.tsx | 5 +++++ src/i18n/strings/en_EN.json | 1 + 3 files changed, 7 insertions(+) diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index fe4196187fd..54f956b801e 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -298,6 +298,7 @@ export default class LoginWithQR extends React.Component { }; private onFailure = (reason: RendezvousFailureReason): void => { + if (this.state.phase === Phase.Error) return; // Already in failed state logger.info(`Rendezvous failed: ${reason}`); this.setState({ phase: Phase.Error, failureReason: reason }); }; diff --git a/src/components/views/auth/LoginWithQRFlow.tsx b/src/components/views/auth/LoginWithQRFlow.tsx index dec4b232428..3f5055187d9 100644 --- a/src/components/views/auth/LoginWithQRFlow.tsx +++ b/src/components/views/auth/LoginWithQRFlow.tsx @@ -156,6 +156,11 @@ export default class LoginWithQRFlow extends React.Component Date: Tue, 23 Apr 2024 19:21:17 +0100 Subject: [PATCH 54/98] Improve coverage Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- test/MatrixClientPeg-test.ts | 22 + .../settings/devices/LoginWithQR-test.tsx | 475 ++++++++++-------- .../devices/LoginWithQRSection-test.tsx | 146 ++++-- 3 files changed, 373 insertions(+), 270 deletions(-) diff --git a/test/MatrixClientPeg-test.ts b/test/MatrixClientPeg-test.ts index 2ed08e0a21f..6349111aa25 100644 --- a/test/MatrixClientPeg-test.ts +++ b/test/MatrixClientPeg-test.ts @@ -21,6 +21,8 @@ import { ProvideCryptoSetupExtensions, SecretStorageKeyDescription, } from "@matrix-org/react-sdk-module-api/lib/lifecycles/CryptoSetupExtensions"; +// eslint-disable-next-line no-restricted-imports +import * as RustCrypto from "matrix-js-sdk/src/rust-crypto"; import { advanceDateAndTime, stubClient } from "./test-utils"; import { IMatrixClientPeg, MatrixClientPeg as peg } from "../src/MatrixClientPeg"; @@ -277,6 +279,26 @@ describe("MatrixClientPeg", () => { expect(mockSetValue).toHaveBeenCalledWith("feature_rust_crypto", null, SettingLevel.DEVICE, false); }); + it("should import qr login secrets when passed", async () => { + fetchMockJest.get("http://example.com/_matrix/client/v3/room_keys/version", { + status: 404, + body: { errcode: "M_NOT_FOUND" }, + }); + const importSecretsForQrLogin = jest.fn(); + jest.spyOn(RustCrypto, "initRustCrypto").mockResolvedValue({ + importSecretsForQrLogin, + setSupportedVerificationMethods: jest.fn(), + onRoomMembership: jest.fn(), + on: jest.fn(), + } as any); + const secrets = { + cross_signing: { master_key: "mk", user_signing_key: "usk", self_signing_key: "ssk" }, + backup: { algorithm: "m.megolm_backup.v1.curve25519-aes-sha2", backup_version: "1", key: "key" }, + }; + await testPeg.start(secrets); + expect(importSecretsForQrLogin).toHaveBeenCalledWith(secrets); + }); + describe("Rust staged rollout", () => { function mockSettingStore( userIsUsingRust: boolean, diff --git a/test/components/views/settings/devices/LoginWithQR-test.tsx b/test/components/views/settings/devices/LoginWithQR-test.tsx index 9b9e33ae87f..2b0a1649e2b 100644 --- a/test/components/views/settings/devices/LoginWithQR-test.tsx +++ b/test/components/views/settings/devices/LoginWithQR-test.tsx @@ -21,6 +21,7 @@ import { MSC3906Rendezvous, LegacyRendezvousFailureReason, ClientRendezvousFailureReason, + MSC4108SignInWithQR, } from "matrix-js-sdk/src/rendezvous"; import { HTTPError, LoginTokenPostResponse } from "matrix-js-sdk/src/matrix"; @@ -77,29 +78,10 @@ describe("", () => { const mockRendezvousCode = "mock-rendezvous-code"; const newDeviceId = "new-device-id"; - const getComponent = (props: { client: MatrixClient; onFinished?: () => void }) => ( - - - - ); - beforeEach(() => { mockedFlow.mockReset(); jest.resetAllMocks(); client = makeClient(); - jest.spyOn(MSC3906Rendezvous.prototype, "generateCode").mockResolvedValue(); - // @ts-ignore - // workaround for https://github.com/facebook/jest/issues/9675 - MSC3906Rendezvous.prototype.code = mockRendezvousCode; - jest.spyOn(MSC3906Rendezvous.prototype, "cancel").mockResolvedValue(); - jest.spyOn(MSC3906Rendezvous.prototype, "startAfterShowingCode").mockResolvedValue(mockConfirmationDigits); - jest.spyOn(MSC3906Rendezvous.prototype, "declineLoginOnExistingDevice").mockResolvedValue(); - jest.spyOn(MSC3906Rendezvous.prototype, "approveLoginOnExistingDevice").mockResolvedValue(newDeviceId); - jest.spyOn(MSC3906Rendezvous.prototype, "verifyNewDeviceOnExistingDevice").mockResolvedValue(undefined); - client.requestLoginToken.mockResolvedValue({ - login_token: "token", - expires_in_ms: 1000 * 1000, - } as LoginTokenPostResponse); // we force the type here so that it works with versions of js-sdk that don't have r1 support yet }); afterEach(() => { @@ -109,234 +91,289 @@ describe("", () => { cleanup(); }); - test("no homeserver support", async () => { - // simulate no support - jest.spyOn(MSC3906Rendezvous.prototype, "generateCode").mockRejectedValue(""); - render(getComponent({ client })); - await waitFor(() => - expect(mockedFlow).toHaveBeenLastCalledWith({ - phase: Phase.Error, - failureReason: LegacyRendezvousFailureReason.HomeserverLacksSupport, - onClick: expect.any(Function), - }), + describe("MSC3906", () => { + const getComponent = (props: { client: MatrixClient; onFinished?: () => void }) => ( + + + ); - const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0]; - expect(rendezvous.generateCode).toHaveBeenCalled(); - }); - test("failed to connect", async () => { - jest.spyOn(MSC3906Rendezvous.prototype, "startAfterShowingCode").mockRejectedValue(""); - render(getComponent({ client })); - await waitFor(() => - expect(mockedFlow).toHaveBeenLastCalledWith({ - phase: Phase.Error, - failureReason: ClientRendezvousFailureReason.Unknown, - onClick: expect.any(Function), - }), - ); - const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0]; - expect(rendezvous.generateCode).toHaveBeenCalled(); - expect(rendezvous.startAfterShowingCode).toHaveBeenCalled(); - }); - - test("render QR then back", async () => { - const onFinished = jest.fn(); - jest.spyOn(MSC3906Rendezvous.prototype, "startAfterShowingCode").mockReturnValue(unresolvedPromise()); - render(getComponent({ client, onFinished })); - const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0]; + beforeEach(() => { + jest.spyOn(MSC3906Rendezvous.prototype, "generateCode").mockResolvedValue(); + // @ts-ignore + // workaround for https://github.com/facebook/jest/issues/9675 + MSC3906Rendezvous.prototype.code = mockRendezvousCode; + jest.spyOn(MSC3906Rendezvous.prototype, "cancel").mockResolvedValue(); + jest.spyOn(MSC3906Rendezvous.prototype, "startAfterShowingCode").mockResolvedValue(mockConfirmationDigits); + jest.spyOn(MSC3906Rendezvous.prototype, "declineLoginOnExistingDevice").mockResolvedValue(); + jest.spyOn(MSC3906Rendezvous.prototype, "approveLoginOnExistingDevice").mockResolvedValue(newDeviceId); + jest.spyOn(MSC3906Rendezvous.prototype, "verifyNewDeviceOnExistingDevice").mockResolvedValue(undefined); + client.requestLoginToken.mockResolvedValue({ + login_token: "token", + expires_in_ms: 1000 * 1000, + } as LoginTokenPostResponse); // we force the type here so that it works with versions of js-sdk that don't have r1 support yet + }); - await waitFor(() => - expect(mockedFlow).toHaveBeenLastCalledWith( - expect.objectContaining({ - phase: Phase.ShowingQR, + test("no homeserver support", async () => { + // simulate no support + jest.spyOn(MSC3906Rendezvous.prototype, "generateCode").mockRejectedValue(""); + render(getComponent({ client })); + await waitFor(() => + expect(mockedFlow).toHaveBeenLastCalledWith({ + phase: Phase.Error, + failureReason: LegacyRendezvousFailureReason.HomeserverLacksSupport, + onClick: expect.any(Function), }), - ), - ); - // display QR code - expect(mockedFlow).toHaveBeenLastCalledWith({ - phase: Phase.ShowingQR, - code: mockRendezvousCode, - onClick: expect.any(Function), + ); + const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0]; + expect(rendezvous.generateCode).toHaveBeenCalled(); }); - expect(rendezvous.generateCode).toHaveBeenCalled(); - expect(rendezvous.startAfterShowingCode).toHaveBeenCalled(); - - // back - const onClick = mockedFlow.mock.calls[0][0].onClick; - await onClick(Click.Back); - expect(onFinished).toHaveBeenCalledWith(false); - expect(rendezvous.cancel).toHaveBeenCalledWith(LegacyRendezvousFailureReason.UserCancelled); - }); - - test("render QR then decline", async () => { - const onFinished = jest.fn(); - render(getComponent({ client, onFinished })); - const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0]; - await waitFor(() => - expect(mockedFlow).toHaveBeenLastCalledWith( - expect.objectContaining({ - phase: Phase.LegacyConnected, + test("failed to connect", async () => { + jest.spyOn(MSC3906Rendezvous.prototype, "startAfterShowingCode").mockRejectedValue(""); + render(getComponent({ client })); + await waitFor(() => + expect(mockedFlow).toHaveBeenLastCalledWith({ + phase: Phase.Error, + failureReason: ClientRendezvousFailureReason.Unknown, + onClick: expect.any(Function), }), - ), - ); - expect(mockedFlow).toHaveBeenLastCalledWith({ - phase: Phase.LegacyConnected, - confirmationDigits: mockConfirmationDigits, - onClick: expect.any(Function), + ); + const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0]; + expect(rendezvous.generateCode).toHaveBeenCalled(); + expect(rendezvous.startAfterShowingCode).toHaveBeenCalled(); }); - // decline - const onClick = mockedFlow.mock.calls[0][0].onClick; - await onClick(Click.Decline); - expect(onFinished).toHaveBeenCalledWith(false); + test("render QR then back", async () => { + const onFinished = jest.fn(); + jest.spyOn(MSC3906Rendezvous.prototype, "startAfterShowingCode").mockReturnValue(unresolvedPromise()); + render(getComponent({ client, onFinished })); + const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0]; + + await waitFor(() => + expect(mockedFlow).toHaveBeenLastCalledWith( + expect.objectContaining({ + phase: Phase.ShowingQR, + }), + ), + ); + // display QR code + expect(mockedFlow).toHaveBeenLastCalledWith({ + phase: Phase.ShowingQR, + code: mockRendezvousCode, + onClick: expect.any(Function), + }); + expect(rendezvous.generateCode).toHaveBeenCalled(); + expect(rendezvous.startAfterShowingCode).toHaveBeenCalled(); + + // back + const onClick = mockedFlow.mock.calls[0][0].onClick; + await onClick(Click.Back); + expect(onFinished).toHaveBeenCalledWith(false); + expect(rendezvous.cancel).toHaveBeenCalledWith(LegacyRendezvousFailureReason.UserCancelled); + }); - expect(rendezvous.generateCode).toHaveBeenCalled(); - expect(rendezvous.startAfterShowingCode).toHaveBeenCalled(); - expect(rendezvous.declineLoginOnExistingDevice).toHaveBeenCalled(); - }); + test("render QR then decline", async () => { + const onFinished = jest.fn(); + render(getComponent({ client, onFinished })); + const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0]; + + await waitFor(() => + expect(mockedFlow).toHaveBeenLastCalledWith( + expect.objectContaining({ + phase: Phase.LegacyConnected, + }), + ), + ); + expect(mockedFlow).toHaveBeenLastCalledWith({ + phase: Phase.LegacyConnected, + confirmationDigits: mockConfirmationDigits, + onClick: expect.any(Function), + }); - test("approve - no crypto", async () => { - (client as any).crypto = undefined; - (client as any).getCrypto = () => undefined; - const onFinished = jest.fn(); - render(getComponent({ client, onFinished })); - const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0]; - - await waitFor(() => - expect(mockedFlow).toHaveBeenLastCalledWith( - expect.objectContaining({ - phase: Phase.LegacyConnected, - }), - ), - ); - expect(mockedFlow).toHaveBeenLastCalledWith({ - phase: Phase.LegacyConnected, - confirmationDigits: mockConfirmationDigits, - onClick: expect.any(Function), - }); - expect(rendezvous.generateCode).toHaveBeenCalled(); - expect(rendezvous.startAfterShowingCode).toHaveBeenCalled(); + // decline + const onClick = mockedFlow.mock.calls[0][0].onClick; + await onClick(Click.Decline); + expect(onFinished).toHaveBeenCalledWith(false); - // approve - const onClick = mockedFlow.mock.calls[0][0].onClick; - await onClick(Click.Approve); + expect(rendezvous.generateCode).toHaveBeenCalled(); + expect(rendezvous.startAfterShowingCode).toHaveBeenCalled(); + expect(rendezvous.declineLoginOnExistingDevice).toHaveBeenCalled(); + }); - await waitFor(() => - expect(mockedFlow).toHaveBeenLastCalledWith( - expect.objectContaining({ - phase: Phase.WaitingForDevice, - }), - ), - ); + test("approve - no crypto", async () => { + (client as any).crypto = undefined; + (client as any).getCrypto = () => undefined; + const onFinished = jest.fn(); + render(getComponent({ client, onFinished })); + const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0]; + + await waitFor(() => + expect(mockedFlow).toHaveBeenLastCalledWith( + expect.objectContaining({ + phase: Phase.LegacyConnected, + }), + ), + ); + expect(mockedFlow).toHaveBeenLastCalledWith({ + phase: Phase.LegacyConnected, + confirmationDigits: mockConfirmationDigits, + onClick: expect.any(Function), + }); + expect(rendezvous.generateCode).toHaveBeenCalled(); + expect(rendezvous.startAfterShowingCode).toHaveBeenCalled(); - expect(rendezvous.approveLoginOnExistingDevice).toHaveBeenCalledWith("token"); + // approve + const onClick = mockedFlow.mock.calls[0][0].onClick; + await onClick(Click.Approve); - expect(onFinished).toHaveBeenCalledWith(true); - }); + await waitFor(() => + expect(mockedFlow).toHaveBeenLastCalledWith( + expect.objectContaining({ + phase: Phase.WaitingForDevice, + }), + ), + ); - test("approve + verifying", async () => { - const onFinished = jest.fn(); - jest.spyOn(MSC3906Rendezvous.prototype, "verifyNewDeviceOnExistingDevice").mockImplementation(() => - unresolvedPromise(), - ); - render(getComponent({ client, onFinished })); - const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0]; + expect(rendezvous.approveLoginOnExistingDevice).toHaveBeenCalledWith("token"); - await waitFor(() => - expect(mockedFlow).toHaveBeenLastCalledWith( - expect.objectContaining({ - phase: Phase.LegacyConnected, - }), - ), - ); - expect(mockedFlow).toHaveBeenLastCalledWith({ - phase: Phase.LegacyConnected, - confirmationDigits: mockConfirmationDigits, - onClick: expect.any(Function), + expect(onFinished).toHaveBeenCalledWith(true); }); - expect(rendezvous.generateCode).toHaveBeenCalled(); - expect(rendezvous.startAfterShowingCode).toHaveBeenCalled(); - // approve - const onClick = mockedFlow.mock.calls[0][0].onClick; - onClick(Click.Approve); + test("approve + verifying", async () => { + const onFinished = jest.fn(); + jest.spyOn(MSC3906Rendezvous.prototype, "verifyNewDeviceOnExistingDevice").mockImplementation(() => + unresolvedPromise(), + ); + render(getComponent({ client, onFinished })); + const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0]; + + await waitFor(() => + expect(mockedFlow).toHaveBeenLastCalledWith( + expect.objectContaining({ + phase: Phase.LegacyConnected, + }), + ), + ); + expect(mockedFlow).toHaveBeenLastCalledWith({ + phase: Phase.LegacyConnected, + confirmationDigits: mockConfirmationDigits, + onClick: expect.any(Function), + }); + expect(rendezvous.generateCode).toHaveBeenCalled(); + expect(rendezvous.startAfterShowingCode).toHaveBeenCalled(); + + // approve + const onClick = mockedFlow.mock.calls[0][0].onClick; + onClick(Click.Approve); + + await waitFor(() => + expect(mockedFlow).toHaveBeenLastCalledWith( + expect.objectContaining({ + phase: Phase.Verifying, + }), + ), + ); + + expect(rendezvous.approveLoginOnExistingDevice).toHaveBeenCalledWith("token"); + expect(rendezvous.verifyNewDeviceOnExistingDevice).toHaveBeenCalled(); + // expect(onFinished).toHaveBeenCalledWith(true); + }); - await waitFor(() => - expect(mockedFlow).toHaveBeenLastCalledWith( - expect.objectContaining({ - phase: Phase.Verifying, - }), - ), - ); + test("approve + verify", async () => { + const onFinished = jest.fn(); + render(getComponent({ client, onFinished })); + const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0]; + + await waitFor(() => + expect(mockedFlow).toHaveBeenLastCalledWith( + expect.objectContaining({ + phase: Phase.LegacyConnected, + }), + ), + ); + expect(mockedFlow).toHaveBeenLastCalledWith({ + phase: Phase.LegacyConnected, + confirmationDigits: mockConfirmationDigits, + onClick: expect.any(Function), + }); + expect(rendezvous.generateCode).toHaveBeenCalled(); + expect(rendezvous.startAfterShowingCode).toHaveBeenCalled(); + + // approve + const onClick = mockedFlow.mock.calls[0][0].onClick; + await onClick(Click.Approve); + expect(rendezvous.approveLoginOnExistingDevice).toHaveBeenCalledWith("token"); + expect(rendezvous.verifyNewDeviceOnExistingDevice).toHaveBeenCalled(); + expect(rendezvous.close).toHaveBeenCalled(); + expect(onFinished).toHaveBeenCalledWith(true); + }); - expect(rendezvous.approveLoginOnExistingDevice).toHaveBeenCalledWith("token"); - expect(rendezvous.verifyNewDeviceOnExistingDevice).toHaveBeenCalled(); - // expect(onFinished).toHaveBeenCalledWith(true); + test("approve - rate limited", async () => { + mocked(client.requestLoginToken).mockRejectedValue(new HTTPError("rate limit reached", 429)); + const onFinished = jest.fn(); + render(getComponent({ client, onFinished })); + const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0]; + + await waitFor(() => + expect(mockedFlow).toHaveBeenLastCalledWith( + expect.objectContaining({ + phase: Phase.LegacyConnected, + }), + ), + ); + expect(mockedFlow).toHaveBeenLastCalledWith({ + phase: Phase.LegacyConnected, + confirmationDigits: mockConfirmationDigits, + onClick: expect.any(Function), + }); + expect(rendezvous.generateCode).toHaveBeenCalled(); + expect(rendezvous.startAfterShowingCode).toHaveBeenCalled(); + + // approve + const onClick = mockedFlow.mock.calls[0][0].onClick; + await onClick(Click.Approve); + + // the 429 error should be handled and mapped + await waitFor(() => + expect(mockedFlow).toHaveBeenLastCalledWith( + expect.objectContaining({ + phase: Phase.Error, + failureReason: "rate_limited", + }), + ), + ); + }); }); - test("approve + verify", async () => { - const onFinished = jest.fn(); - render(getComponent({ client, onFinished })); - const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0]; - - await waitFor(() => - expect(mockedFlow).toHaveBeenLastCalledWith( - expect.objectContaining({ - phase: Phase.LegacyConnected, - }), - ), + describe("MSC4108", () => { + const getComponent = (props: { client: MatrixClient; onFinished?: () => void }) => ( + + + ); - expect(mockedFlow).toHaveBeenLastCalledWith({ - phase: Phase.LegacyConnected, - confirmationDigits: mockConfirmationDigits, - onClick: expect.any(Function), - }); - expect(rendezvous.generateCode).toHaveBeenCalled(); - expect(rendezvous.startAfterShowingCode).toHaveBeenCalled(); - - // approve - const onClick = mockedFlow.mock.calls[0][0].onClick; - await onClick(Click.Approve); - expect(rendezvous.approveLoginOnExistingDevice).toHaveBeenCalledWith("token"); - expect(rendezvous.verifyNewDeviceOnExistingDevice).toHaveBeenCalled(); - expect(rendezvous.close).toHaveBeenCalled(); - expect(onFinished).toHaveBeenCalledWith(true); - }); - test("approve - rate limited", async () => { - mocked(client.requestLoginToken).mockRejectedValue(new HTTPError("rate limit reached", 429)); - const onFinished = jest.fn(); - render(getComponent({ client, onFinished })); - const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0]; + test("render QR then back", async () => { + const onFinished = jest.fn(); + jest.spyOn(MSC4108SignInWithQR.prototype, "loginStep1").mockReturnValue(unresolvedPromise()); + render(getComponent({ client, onFinished })); - await waitFor(() => - expect(mockedFlow).toHaveBeenLastCalledWith( - expect.objectContaining({ - phase: Phase.LegacyConnected, + await waitFor(() => + expect(mockedFlow).toHaveBeenLastCalledWith({ + phase: Phase.ShowingQR, + onClick: expect.any(Function), }), - ), - ); - expect(mockedFlow).toHaveBeenLastCalledWith({ - phase: Phase.LegacyConnected, - confirmationDigits: mockConfirmationDigits, - onClick: expect.any(Function), - }); - expect(rendezvous.generateCode).toHaveBeenCalled(); - expect(rendezvous.startAfterShowingCode).toHaveBeenCalled(); + ); - // approve - const onClick = mockedFlow.mock.calls[0][0].onClick; - await onClick(Click.Approve); + const rendezvous = mocked(MSC4108SignInWithQR).mock.instances[0]; + expect(rendezvous.generateCode).toHaveBeenCalled(); + expect(rendezvous.loginStep1).toHaveBeenCalled(); - // the 429 error should be handled and mapped - await waitFor(() => - expect(mockedFlow).toHaveBeenLastCalledWith( - expect.objectContaining({ - phase: Phase.Error, - failureReason: "rate_limited", - }), - ), - ); + // back + const onClick = mockedFlow.mock.calls[0][0].onClick; + await onClick(Click.Back); + expect(onFinished).toHaveBeenCalledWith(false); + expect(rendezvous.cancel).toHaveBeenCalledWith(LegacyRendezvousFailureReason.UserCancelled); + }); }); }); diff --git a/test/components/views/settings/devices/LoginWithQRSection-test.tsx b/test/components/views/settings/devices/LoginWithQRSection-test.tsx index 8dc78bfd28d..495116cdfde 100644 --- a/test/components/views/settings/devices/LoginWithQRSection-test.tsx +++ b/test/components/views/settings/devices/LoginWithQRSection-test.tsx @@ -18,11 +18,17 @@ import { render } from "@testing-library/react"; import { mocked } from "jest-mock"; import { IClientWellKnown, IServerVersions, MatrixClient, GET_LOGIN_TOKEN_CAPABILITY } from "matrix-js-sdk/src/matrix"; import React from "react"; +import fetchMock from "fetch-mock-jest"; import LoginWithQRSection from "../../../../../src/components/views/settings/devices/LoginWithQRSection"; import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg"; function makeClient(wellKnown: IClientWellKnown) { + const crypto = mocked({ + supportsSecretsForQrLogin: jest.fn().mockReturnValue(true), + isCrossSigningReady: jest.fn().mockReturnValue(true), + }); + return mocked({ getUser: jest.fn(), isGuest: jest.fn().mockReturnValue(false), @@ -38,6 +44,7 @@ function makeClient(wellKnown: IClientWellKnown) { on: jest.fn(), }, getClientWellKnown: jest.fn().mockReturnValue(wellKnown), + getCrypto: jest.fn().mockReturnValue(crypto), } as unknown as MatrixClient); } @@ -53,68 +60,105 @@ describe("", () => { jest.spyOn(MatrixClientPeg, "get").mockReturnValue(makeClient({})); }); - const defaultProps = { - onShowQr: () => {}, - versions: makeVersions({}), - wellKnown: {}, - }; + describe("MSC3906", () => { + const defaultProps = { + onShowQr: () => {}, + versions: makeVersions({}), + wellKnown: {}, + }; - const getComponent = (props = {}) => ; + const getComponent = (props = {}) => ; - describe("should not render", () => { - it("no support at all", () => { - const { container } = render(getComponent()); - expect(container).toMatchSnapshot(); - }); + describe("should not render", () => { + it("no support at all", () => { + const { container } = render(getComponent()); + expect(container).toMatchSnapshot(); + }); - it("only get_login_token enabled", async () => { - const { container } = render( - getComponent({ capabilities: { [GET_LOGIN_TOKEN_CAPABILITY.name]: { enabled: true } } }), - ); - expect(container).toMatchSnapshot(); - }); + it("only get_login_token enabled", async () => { + const { container } = render( + getComponent({ capabilities: { [GET_LOGIN_TOKEN_CAPABILITY.name]: { enabled: true } } }), + ); + expect(container).toMatchSnapshot(); + }); - it("MSC3886 + get_login_token disabled", async () => { - const { container } = render( - getComponent({ - versions: makeVersions({ "org.matrix.msc3886": true }), - capabilities: { [GET_LOGIN_TOKEN_CAPABILITY.name]: { enabled: false } }, - }), - ); - expect(container).toMatchSnapshot(); + it("MSC3886 + get_login_token disabled", async () => { + const { container } = render( + getComponent({ + versions: makeVersions({ "org.matrix.msc3886": true }), + capabilities: { [GET_LOGIN_TOKEN_CAPABILITY.name]: { enabled: false } }, + }), + ); + expect(container).toMatchSnapshot(); + }); }); - }); - describe("should render panel", () => { - it("get_login_token + MSC3886", async () => { - const { container } = render( - getComponent({ - versions: makeVersions({ - "org.matrix.msc3886": true, + describe("should render panel", () => { + it("get_login_token + MSC3886", async () => { + const { container } = render( + getComponent({ + versions: makeVersions({ + "org.matrix.msc3886": true, + }), + capabilities: { + [GET_LOGIN_TOKEN_CAPABILITY.name]: { enabled: true }, + }, }), - capabilities: { - [GET_LOGIN_TOKEN_CAPABILITY.name]: { enabled: true }, + ); + expect(container).toMatchSnapshot(); + }); + + it("get_login_token + .well-known", async () => { + const wellKnown = { + "io.element.rendezvous": { + server: "https://rz.local", }, - }), - ); - expect(container).toMatchSnapshot(); + }; + jest.spyOn(MatrixClientPeg, "get").mockReturnValue(makeClient(wellKnown)); + const { container } = render( + getComponent({ + versions: makeVersions({}), + capabilities: { [GET_LOGIN_TOKEN_CAPABILITY.name]: { enabled: true } }, + wellKnown, + }), + ); + expect(container).toMatchSnapshot(); + }); }); + }); - it("get_login_token + .well-known", async () => { - const wellKnown = { - "io.element.rendezvous": { - server: "https://rz.local", - }, + describe("MSC4108", () => { + describe("MSC4108", () => { + const defaultProps = { + onShowQr: () => {}, + versions: makeVersions({ "org.matrix.msc4108": true }), + wellKnown: {}, }; - jest.spyOn(MatrixClientPeg, "get").mockReturnValue(makeClient(wellKnown)); - const { container } = render( - getComponent({ - versions: makeVersions({}), - capabilities: { [GET_LOGIN_TOKEN_CAPABILITY.name]: { enabled: true } }, - wellKnown, - }), - ); - expect(container).toMatchSnapshot(); + + const getComponent = (props = {}) => ; + + let client: MatrixClient; + beforeEach(() => { + client = makeClient({}); + jest.spyOn(MatrixClientPeg, "get").mockReturnValue(client); + }); + + test("no homeserver support", async () => { + const { container } = render(getComponent({ versions: makeVersions({ "org.matrix.msc4108": false }) })); + expect(container.textContent).toBe(""); // show nothing + }); + + test("no support in crypto", async () => { + mocked(client.getCrypto()!.supportsSecretsForQrLogin).mockReturnValue(false); + const { container } = render(getComponent({ client })); + expect(container.textContent).toBe(""); // show nothing + }); + + test("failed to connect", async () => { + fetchMock.catch(500); + const { container } = render(getComponent({ client })); + expect(container.textContent).toBe(""); // show nothing + }); }); }); }); From eb89328b30c2d79688f33d78a2861e4f40df5ea4 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 24 Apr 2024 09:43:11 +0100 Subject: [PATCH 55/98] Update snapshots Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../__snapshots__/LoginWithQRSection-test.tsx.snap | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/components/views/settings/devices/__snapshots__/LoginWithQRSection-test.tsx.snap b/test/components/views/settings/devices/__snapshots__/LoginWithQRSection-test.tsx.snap index 78ce99e6991..f1152f4cc2d 100644 --- a/test/components/views/settings/devices/__snapshots__/LoginWithQRSection-test.tsx.snap +++ b/test/components/views/settings/devices/__snapshots__/LoginWithQRSection-test.tsx.snap @@ -1,12 +1,12 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[` should not render MSC3886 + get_login_token disabled 1`] = `
`; +exports[` MSC3906 should not render MSC3886 + get_login_token disabled 1`] = `
`; -exports[` should not render no support at all 1`] = `
`; +exports[` MSC3906 should not render no support at all 1`] = `
`; -exports[` should not render only get_login_token enabled 1`] = `
`; +exports[` MSC3906 should not render only get_login_token enabled 1`] = `
`; -exports[` should render panel get_login_token + .well-known 1`] = ` +exports[` MSC3906 should render panel get_login_token + .well-known 1`] = `
should render panel get_login_token + .well-know
`; -exports[` should render panel get_login_token + MSC3886 1`] = ` +exports[` MSC3906 should render panel get_login_token + MSC3886 1`] = `
Date: Wed, 24 Apr 2024 09:53:18 +0100 Subject: [PATCH 56/98] Improve coverage Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../views/settings/devices/LoginWithQR-test.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/components/views/settings/devices/LoginWithQR-test.tsx b/test/components/views/settings/devices/LoginWithQR-test.tsx index 2b0a1649e2b..ac11c32cd09 100644 --- a/test/components/views/settings/devices/LoginWithQR-test.tsx +++ b/test/components/views/settings/devices/LoginWithQR-test.tsx @@ -375,5 +375,15 @@ describe("", () => { expect(onFinished).toHaveBeenCalledWith(false); expect(rendezvous.cancel).toHaveBeenCalledWith(LegacyRendezvousFailureReason.UserCancelled); }); + + test("failed to connect", async () => { + render(getComponent({ client })); + jest.spyOn(MSC4108SignInWithQR.prototype, "loginStep1").mockResolvedValue({}); + jest.spyOn(MSC4108SignInWithQR.prototype, "loginStep2And3").mockRejectedValue( + new HTTPError("Internal Server Error", 500), + ); + const fn = jest.spyOn(MSC4108SignInWithQR.prototype, "cancel"); + await waitFor(() => expect(fn).toHaveBeenLastCalledWith(ClientRendezvousFailureReason.Unknown)); + }); }); }); From 9150366ec1807eac0b104c1efac48e11bc6fead9 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 24 Apr 2024 10:32:49 +0100 Subject: [PATCH 57/98] Improve coverage Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/auth/LoginWithQRFlow.tsx | 2 +- .../settings/devices/LoginWithQR-test.tsx | 25 ++ .../settings/devices/LoginWithQRFlow-test.tsx | 12 +- .../LoginWithQRFlow-test.tsx.snap | 393 ++++++++++++++++++ 4 files changed, 430 insertions(+), 2 deletions(-) diff --git a/src/components/views/auth/LoginWithQRFlow.tsx b/src/components/views/auth/LoginWithQRFlow.tsx index 3f5055187d9..14e3688e6f3 100644 --- a/src/components/views/auth/LoginWithQRFlow.tsx +++ b/src/components/views/auth/LoginWithQRFlow.tsx @@ -128,7 +128,7 @@ export default class LoginWithQRFlow extends React.Component {_t("auth|qr_code_login|error_insecure_channel_detected")} - + {_t("auth|qr_code_login|error_insecure_channel_detected_instructions")}
    diff --git a/test/components/views/settings/devices/LoginWithQR-test.tsx b/test/components/views/settings/devices/LoginWithQR-test.tsx index ac11c32cd09..f24c12bd954 100644 --- a/test/components/views/settings/devices/LoginWithQR-test.tsx +++ b/test/components/views/settings/devices/LoginWithQR-test.tsx @@ -385,5 +385,30 @@ describe("", () => { const fn = jest.spyOn(MSC4108SignInWithQR.prototype, "cancel"); await waitFor(() => expect(fn).toHaveBeenLastCalledWith(ClientRendezvousFailureReason.Unknown)); }); + + test("reciprocates login", async () => { + render(getComponent({ client })); + jest.spyOn(MSC4108SignInWithQR.prototype, "loginStep1").mockResolvedValue({}); + jest.spyOn(MSC4108SignInWithQR.prototype, "loginStep2And3").mockResolvedValue({ + verificationUri: "mock-verification-uri", + }); + + await waitFor(() => + expect(mockedFlow).toHaveBeenLastCalledWith({ + phase: Phase.OutOfBandConfirmation, + onClick: expect.any(Function), + }), + ); + + const onClick = mockedFlow.mock.calls[0][0].onClick; + await onClick(Click.Approve); + + await waitFor(() => + expect(mockedFlow).toHaveBeenLastCalledWith({ + phase: Phase.WaitingForDevice, + onClick: expect.any(Function), + }), + ); + }); }); }); diff --git a/test/components/views/settings/devices/LoginWithQRFlow-test.tsx b/test/components/views/settings/devices/LoginWithQRFlow-test.tsx index 1262507afa8..2598c053609 100644 --- a/test/components/views/settings/devices/LoginWithQRFlow-test.tsx +++ b/test/components/views/settings/devices/LoginWithQRFlow-test.tsx @@ -16,7 +16,11 @@ limitations under the License. import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; import React from "react"; -import { LegacyRendezvousFailureReason, MSC4108FailureReason } from "matrix-js-sdk/src/rendezvous"; +import { + ClientRendezvousFailureReason, + LegacyRendezvousFailureReason, + MSC4108FailureReason, +} from "matrix-js-sdk/src/rendezvous"; import LoginWithQRFlow from "../../../../../src/components/views/auth/LoginWithQRFlow"; import { LoginWithQRFailureReason, FailureReason } from "../../../../../src/components/views/auth/LoginWithQR"; @@ -97,11 +101,17 @@ describe("", () => { expect(container).toMatchSnapshot(); }); + it("renders check code confirmation", async () => { + const { container } = render(getComponent({ phase: Phase.OutOfBandConfirmation })); + expect(container).toMatchSnapshot(); + }); + describe("errors", () => { for (const failureReason of [ ...Object.values(LegacyRendezvousFailureReason), ...Object.values(MSC4108FailureReason), ...Object.values(LoginWithQRFailureReason), + ...Object.values(ClientRendezvousFailureReason), ]) { it(`renders ${failureReason}`, async () => { const { container } = render( diff --git a/test/components/views/settings/devices/__snapshots__/LoginWithQRFlow-test.tsx.snap b/test/components/views/settings/devices/__snapshots__/LoginWithQRFlow-test.tsx.snap index 75d91ee40e9..198e59049ab 100644 --- a/test/components/views/settings/devices/__snapshots__/LoginWithQRFlow-test.tsx.snap +++ b/test/components/views/settings/devices/__snapshots__/LoginWithQRFlow-test.tsx.snap @@ -136,6 +136,40 @@ exports[` errors renders device_not_found 1`] = `
`; +exports[` errors renders etag_missing 1`] = ` +
+
+
+
+
+
+

+ Something went wrong! +

+

+ An unexpected error occurred. This may be due to a browser extension, proxy server, or server misconfiguration. +

+
+
+
+
+`; + exports[` errors renders expired 1`] = `
errors renders expired 1`] = `
`; +exports[` errors renders expired 2`] = ` +
+
+
+
+
+
+

+ The sign in was not completed in time +

+

+ Sign in expired. Please try again. +

+
+
+
+
+`; + exports[` errors renders homeserver_lacks_support 1`] = `
errors renders homeserver_lacks_support 1`] = `
`; +exports[` errors renders homeserver_lacks_support 2`] = ` +
+
+
+
+
+
+

+ Something went wrong! +

+

+ An unexpected error occurred. The request to connect your other device has been cancelled. +

+
+
+
+
+`; + +exports[` errors renders insecure_channel_detected 1`] = ` +
+
+
+
+
+
+

+ Connection not secure +

+ A secure connection could not be made to the new device. Your existing devices are still safe and you don't need to worry about them. +

+ Now what? +

+
    +
  1. + Try signing in to the other device again with a QR code in case this was a network problem +
  2. +
  3. + If you encounter the same problem, try a different wifi network or use your mobile data instead of wifi +
  4. +
  5. + If that doesn't work, sign in manually +
  6. +
+
+
+
+
+`; + +exports[` errors renders invalid_code 1`] = ` +
+
+
+
+
+
+

+ Something went wrong! +

+

+ An unexpected error occurred. The request to connect your other device has been cancelled. +

+
+
+
+
+`; + +exports[` errors renders other_device_already_signed_in 1`] = ` +
+
+
+
+
+
+

+ Your other device is already signed in +

+

+ You don’t need to do anything else. +

+
+
+
+
+`; + +exports[` errors renders other_device_not_signed_in 1`] = ` +
+
+
+
+
+
+

+ Something went wrong! +

+

+ An unexpected error occurred. The request to connect your other device has been cancelled. +

+
+
+
+
+`; + exports[` errors renders rate_limited 1`] = `
errors renders unknown 1`] = `
`; +exports[` errors renders unknown 2`] = ` +
+
+
+
+
+
+

+ Something went wrong! +

+

+ An unexpected error occurred. The request to connect your other device has been cancelled. +

+
+
+
+
+`; + exports[` errors renders unsupported_algorithm 1`] = `
errors renders user_declined 1`] = `
`; +exports[` errors renders user_declined 2`] = ` +
+
+
+
+
+
+

+ Sign in declined +

+

+ You declined the request from your other device to sign in. +

+
+
+
+
+`; + exports[` renders QR code 1`] = `
renders QR code 1`] = `
`; +exports[` renders check code confirmation 1`] = ` +
+
+
+

+ Do you see a green checkmark on your other device? +

+

+ Confirm that you see a green checkmark on the other device to verify that the connection is secure. +

+ +
+ + +
+`; + exports[` renders code when connected 1`] = `
Date: Wed, 24 Apr 2024 15:14:29 +0100 Subject: [PATCH 58/98] Improve coverage Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../settings/devices/LoginWithQR-test.tsx | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/test/components/views/settings/devices/LoginWithQR-test.tsx b/test/components/views/settings/devices/LoginWithQR-test.tsx index f24c12bd954..caeb21a3a85 100644 --- a/test/components/views/settings/devices/LoginWithQR-test.tsx +++ b/test/components/views/settings/devices/LoginWithQR-test.tsx @@ -387,6 +387,8 @@ describe("", () => { }); test("reciprocates login", async () => { + jest.spyOn(global.window, "open"); + render(getComponent({ client })); jest.spyOn(MSC4108SignInWithQR.prototype, "loginStep1").mockResolvedValue({}); jest.spyOn(MSC4108SignInWithQR.prototype, "loginStep2And3").mockResolvedValue({ @@ -409,6 +411,34 @@ describe("", () => { onClick: expect.any(Function), }), ); + expect(global.window.open).toHaveBeenCalledWith("mock-verification-uri", "_blank"); + }); + + test("handles errors during reciprocation", async () => { + render(getComponent({ client })); + jest.spyOn(MSC4108SignInWithQR.prototype, "loginStep1").mockResolvedValue({}); + jest.spyOn(MSC4108SignInWithQR.prototype, "loginStep2And3").mockResolvedValue({}); + await waitFor(() => + expect(mockedFlow).toHaveBeenLastCalledWith({ + phase: Phase.OutOfBandConfirmation, + onClick: expect.any(Function), + }), + ); + + jest.spyOn(MSC4108SignInWithQR.prototype, "loginStep5").mockRejectedValue( + new HTTPError("Internal Server Error", 500), + ); + const onClick = mockedFlow.mock.calls[0][0].onClick; + await onClick(Click.Approve); + + await waitFor(() => + expect(mockedFlow).toHaveBeenLastCalledWith( + expect.objectContaining({ + phase: Phase.Error, + failureReason: ClientRendezvousFailureReason.Unknown, + }), + ), + ); }); }); }); From efb9e89327b62a7d1c5166deaf2963309253ea1d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 24 Apr 2024 15:43:01 +0100 Subject: [PATCH 59/98] Improve coverage Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../settings/devices/LoginWithQR-test.tsx | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/test/components/views/settings/devices/LoginWithQR-test.tsx b/test/components/views/settings/devices/LoginWithQR-test.tsx index caeb21a3a85..cb0d1f77f26 100644 --- a/test/components/views/settings/devices/LoginWithQR-test.tsx +++ b/test/components/views/settings/devices/LoginWithQR-test.tsx @@ -22,6 +22,7 @@ import { LegacyRendezvousFailureReason, ClientRendezvousFailureReason, MSC4108SignInWithQR, + MSC4108FailureReason, } from "matrix-js-sdk/src/rendezvous"; import { HTTPError, LoginTokenPostResponse } from "matrix-js-sdk/src/matrix"; @@ -440,5 +441,25 @@ describe("", () => { ), ); }); + + test("handles user cancelling during reciprocation", async () => { + render(getComponent({ client })); + jest.spyOn(MSC4108SignInWithQR.prototype, "loginStep1").mockResolvedValue({}); + jest.spyOn(MSC4108SignInWithQR.prototype, "loginStep2And3").mockResolvedValue({}); + jest.spyOn(MSC4108SignInWithQR.prototype, "loginStep2And3").mockResolvedValue({}); + await waitFor(() => + expect(mockedFlow).toHaveBeenLastCalledWith({ + phase: Phase.OutOfBandConfirmation, + onClick: expect.any(Function), + }), + ); + + jest.spyOn(MSC4108SignInWithQR.prototype, "cancel").mockResolvedValue(); + const onClick = mockedFlow.mock.calls[0][0].onClick; + await onClick(Click.Cancel); + + const rendezvous = mocked(MSC4108SignInWithQR).mock.instances[0]; + expect(rendezvous.cancel).toHaveBeenCalledWith(MSC4108FailureReason.UserCancelled); + }); }); }); From aa658a571cecad4f844d6a264f743566cadaf522 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 24 Apr 2024 15:51:03 +0100 Subject: [PATCH 60/98] Improve docs Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/MatrixClientPeg.ts | 38 +++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index caba704229b..2518f478d59 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -75,22 +75,46 @@ export interface IMatrixClientCreds { * you'll find a `MatrixClient` hanging on the `MatrixClientPeg`. */ export interface IMatrixClientPeg { + /** + * The opts used to start the client + */ opts: IStartClientOpts; /** * Return the server name of the user's homeserver * Throws an error if unable to deduce the homeserver name - * (eg. if the user is not logged in) + * (e.g. if the user is not logged in) * * @returns {string} The homeserver name, if present. */ getHomeserverName(): string; + /** + * Get the current MatrixClient, if any + */ get(): MatrixClient | null; + + /** + * Get the current MatrixClient, throwing an error if there isn't one + */ safeGet(): MatrixClient; + + /** + * Unset the current MatrixClient + */ unset(): void; - assign(secrets?: QRSecretsBundle): Promise; - start(secrets?: QRSecretsBundle): Promise; + + /** + * Prepare the MatrixClient for use, including initialising the store and crypto, but do not start it + * @param secrets the secrets to use if authenticating using QR login + */ + assign(secrets?: QRSecretsBundle): Promise; + + /** + * Prepare the MatrixClient for use, including initialising the store and crypto, and start it + * @param secrets the secrets to use if authenticating using QR login + */ + start(secrets?: QRSecretsBundle): Promise; /** * If we've registered a user ID we set this to the ID of the @@ -112,13 +136,13 @@ export interface IMatrixClientPeg { /** * If the current user has been registered by this device then this - * returns a boolean of whether it was within the last N hours given. + * returns boolean of whether it was within the last N hours given. */ userRegisteredWithinLastHours(hours: number): boolean; /** * If the current user has been registered by this device then this - * returns a boolean of whether it was after a given timestamp. + * returns boolean of whether it was after a given timestamp. */ userRegisteredAfter(date: Date): boolean; @@ -237,7 +261,7 @@ class MatrixClientPegClass implements IMatrixClientPeg { PlatformPeg.get()?.reload(); }; - public async assign(secrets?: QRSecretsBundle): Promise { + public async assign(secrets?: QRSecretsBundle): Promise { if (!this.matrixClient) { throw new Error("createClient must be called first"); } @@ -374,7 +398,7 @@ class MatrixClientPegClass implements IMatrixClientPeg { } } - public async start(secrets?: QRSecretsBundle): Promise { + public async start(secrets?: QRSecretsBundle): Promise { const opts = await this.assign(secrets); logger.log(`MatrixClientPeg: really starting MatrixClient`); From 20cae74381065bbe26df398dfe0c0c1bf1e7df71 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 24 Apr 2024 15:55:38 +0100 Subject: [PATCH 61/98] Tidy Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../settings/devices/LoginWithQRSection.tsx | 40 +++++++++++-------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/src/components/views/settings/devices/LoginWithQRSection.tsx b/src/components/views/settings/devices/LoginWithQRSection.tsx index 8e3f4cc961b..138edbc3db1 100644 --- a/src/components/views/settings/devices/LoginWithQRSection.tsx +++ b/src/components/views/settings/devices/LoginWithQRSection.tsx @@ -22,9 +22,9 @@ import { Capabilities, IClientWellKnown, OidcClientConfig, + MatrixClient, } from "matrix-js-sdk/src/matrix"; import { Icon as QrCodeIcon } from "@vector-im/compound-design-tokens/icons/qr-code.svg"; -import { logger } from "matrix-js-sdk/src/logger"; import { _t } from "../../../../languageHandler"; import AccessibleButton from "../../elements/AccessibleButton"; @@ -41,9 +41,7 @@ interface IProps { oidcClientConfig?: OidcClientConfig; } -const LoginWithQRSection: React.FC = ({ onShowQr, versions, capabilities, wellKnown, oidcClientConfig }) => { - const cli = useMatrixClientContext(); - +function showQrLegacy(versions?: IServerVersions, wellKnown?: IClientWellKnown, capabilities?: Capabilities): boolean { // Needs server support for (get_login_token or OIDC Device Authorization Grant) and MSC3886: // in r0 of MSC3882 it is exposed as a feature flag, but in stable and unstable r1 it is a capability const loginTokenCapability = GET_LOGIN_TOKEN_CAPABILITY.findIn(capabilities); @@ -51,6 +49,15 @@ const LoginWithQRSection: React.FC = ({ onShowQr, versions, capabilities !!versions?.unstable_features?.["org.matrix.msc3882"] || !!loginTokenCapability?.enabled; const msc3886Supported = !!versions?.unstable_features?.["org.matrix.msc3886"] || !!wellKnown?.["io.element.rendezvous"]?.server; + return getLoginTokenSupported && msc3886Supported; +} + +function showQr( + cli: MatrixClient, + oidcClientConfig?: OidcClientConfig, + versions?: IServerVersions, + wellKnown?: IClientWellKnown, +): boolean { const msc4108Supported = !!versions?.unstable_features?.["org.matrix.msc4108"] || !!wellKnown?.["io.element.rendezvous"]?.server; @@ -58,19 +65,20 @@ const LoginWithQRSection: React.FC = ({ onShowQr, versions, capabilities "urn:ietf:params:oauth:grant-type:device_code", ); - logger.debug({ - msc3886Supported, - getLoginTokenSupported, - msc4108Supported, - deviceAuthorizationGrantSupported, - }); + return ( + deviceAuthorizationGrantSupported && + msc4108Supported && + SettingsStore.getValue(Features.OidcNativeFlow) && + cli.getCrypto()?.supportsSecretsForQrLogin() && + cli.getCrypto()?.isCrossSigningReady() + ); +} + +const LoginWithQRSection: React.FC = ({ onShowQr, versions, capabilities, wellKnown, oidcClientConfig }) => { + const cli = useMatrixClientContext(); const offerShowQr = oidcClientConfig - ? deviceAuthorizationGrantSupported && - msc4108Supported && - SettingsStore.getValue(Features.OidcNativeFlow) && - cli.getCrypto()?.supportsSecretsForQrLogin() && - cli.getCrypto()?.isCrossSigningReady() - : getLoginTokenSupported && msc3886Supported; + ? showQr(cli, oidcClientConfig, versions, wellKnown) + : showQrLegacy(versions, wellKnown, capabilities); // don't show anything if no method is available if (!offerShowQr) { From 62fd3bd05b7b20e7ea98d8b7b82f3eadf9b01e24 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 24 Apr 2024 15:59:24 +0100 Subject: [PATCH 62/98] Tidy Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/auth/LoginWithQR-types.ts | 1 - src/components/views/auth/LoginWithQRFlow.tsx | 4 ---- .../views/settings/devices/LoginWithQRFlow-test.tsx | 8 -------- 3 files changed, 13 deletions(-) diff --git a/src/components/views/auth/LoginWithQR-types.ts b/src/components/views/auth/LoginWithQR-types.ts index 3fede49db41..a5bc7047508 100644 --- a/src/components/views/auth/LoginWithQR-types.ts +++ b/src/components/views/auth/LoginWithQR-types.ts @@ -28,7 +28,6 @@ export enum Phase { Loading, ShowingQR, // The following are specific to MSC4108 - Connecting, OutOfBandConfirmation, WaitingForDevice, Verifying, diff --git a/src/components/views/auth/LoginWithQRFlow.tsx b/src/components/views/auth/LoginWithQRFlow.tsx index 14e3688e6f3..6825cb0b465 100644 --- a/src/components/views/auth/LoginWithQRFlow.tsx +++ b/src/components/views/auth/LoginWithQRFlow.tsx @@ -312,10 +312,6 @@ export default class LoginWithQRFlow extends React.Component diff --git a/test/components/views/settings/devices/LoginWithQRFlow-test.tsx b/test/components/views/settings/devices/LoginWithQRFlow-test.tsx index 2598c053609..1f896b28b78 100644 --- a/test/components/views/settings/devices/LoginWithQRFlow-test.tsx +++ b/test/components/views/settings/devices/LoginWithQRFlow-test.tsx @@ -68,14 +68,6 @@ describe("", () => { expect(container).toMatchSnapshot(); }); - it("renders spinner while connecting", async () => { - const { container } = render(getComponent({ phase: Phase.Connecting })); - expect(screen.getAllByTestId("cancel-button")).toHaveLength(1); - expect(container).toMatchSnapshot(); - fireEvent.click(screen.getByTestId("cancel-button")); - expect(onClick).toHaveBeenCalledWith(Click.Cancel, undefined); - }); - it("renders code when connected", async () => { const { container } = render(getComponent({ phase: Phase.LegacyConnected, confirmationDigits: "mock-digits" })); expect(screen.getAllByText("mock-digits")).toHaveLength(1); From ff1d334de2f0efb99268f62122180489734108cf Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 24 Apr 2024 16:31:36 +0100 Subject: [PATCH 63/98] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/i18n/strings/en_EN.json | 1 - .../LoginWithQRFlow-test.tsx.snap | 67 ------------------- 2 files changed, 68 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 180ba1b42b6..f6072cf2a7b 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -252,7 +252,6 @@ "check_code_mismatch": "The numbers don't match", "completing_setup": "Completing set up of your new device", "confirm_code_match": "Check that the code below matches with your other device:", - "connecting": "Connecting…", "error_etag_missing": "An unexpected error occurred. This may be due to a browser extension, proxy server, or server misconfiguration.", "error_expired": "Sign in expired. Please try again.", "error_expired_title": "The sign in was not completed in time", diff --git a/test/components/views/settings/devices/__snapshots__/LoginWithQRFlow-test.tsx.snap b/test/components/views/settings/devices/__snapshots__/LoginWithQRFlow-test.tsx.snap index 2995698bf86..54c263db73f 100644 --- a/test/components/views/settings/devices/__snapshots__/LoginWithQRFlow-test.tsx.snap +++ b/test/components/views/settings/devices/__snapshots__/LoginWithQRFlow-test.tsx.snap @@ -1031,73 +1031,6 @@ exports[` renders code when connected 1`] = `
`; -exports[` renders spinner while connecting 1`] = ` -
-
-
-
-
-
-
- Sessions - / - Link new device -
-
-
-
-
-
-
-
-

- Connecting… -

-
-
-
-
-
- Cancel -
-
-
-
-`; - exports[` renders spinner while loading 1`] = `
Date: Thu, 25 Apr 2024 16:28:30 +0100 Subject: [PATCH 64/98] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/auth/LoginWithQR.tsx | 8 +++---- .../settings/devices/LoginWithQR-test.tsx | 24 +++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index 54f956b801e..522cf6e28eb 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -223,7 +223,7 @@ export default class LoginWithQR extends React.Component { // MSC4108-Flow: ExistingScanned // we get the homserver URL from the secure channel, but we don't trust it yet - const { homeserverBaseUrl } = await rendezvous.loginStep1(); + const { homeserverBaseUrl } = await rendezvous.negotiateProtocols(); if (!homeserverBaseUrl) { throw new Error("We don't know the homeserver"); @@ -234,8 +234,8 @@ export default class LoginWithQR extends React.Component { }); } else { // MSC4108-Flow: NewScanned - await rendezvous.loginStep1(); - const { verificationUri } = await rendezvous.loginStep2And3(); + await rendezvous.negotiateProtocols(); + const { verificationUri } = await rendezvous.deviceAuthorizationGrant(); this.setState({ phase: Phase.OutOfBandConfirmation, verificationUri, @@ -283,7 +283,7 @@ export default class LoginWithQR extends React.Component { this.setState({ phase: Phase.WaitingForDevice }); // send secrets - await this.state.rendezvous.loginStep5(); + await this.state.rendezvous.shareSecrets(); // done this.props.onFinished(true); diff --git a/test/components/views/settings/devices/LoginWithQR-test.tsx b/test/components/views/settings/devices/LoginWithQR-test.tsx index cb0d1f77f26..6e7636d0cfa 100644 --- a/test/components/views/settings/devices/LoginWithQR-test.tsx +++ b/test/components/views/settings/devices/LoginWithQR-test.tsx @@ -356,7 +356,7 @@ describe("", () => { test("render QR then back", async () => { const onFinished = jest.fn(); - jest.spyOn(MSC4108SignInWithQR.prototype, "loginStep1").mockReturnValue(unresolvedPromise()); + jest.spyOn(MSC4108SignInWithQR.prototype, "negotiateProtocols").mockReturnValue(unresolvedPromise()); render(getComponent({ client, onFinished })); await waitFor(() => @@ -368,7 +368,7 @@ describe("", () => { const rendezvous = mocked(MSC4108SignInWithQR).mock.instances[0]; expect(rendezvous.generateCode).toHaveBeenCalled(); - expect(rendezvous.loginStep1).toHaveBeenCalled(); + expect(rendezvous.negotiateProtocols).toHaveBeenCalled(); // back const onClick = mockedFlow.mock.calls[0][0].onClick; @@ -379,8 +379,8 @@ describe("", () => { test("failed to connect", async () => { render(getComponent({ client })); - jest.spyOn(MSC4108SignInWithQR.prototype, "loginStep1").mockResolvedValue({}); - jest.spyOn(MSC4108SignInWithQR.prototype, "loginStep2And3").mockRejectedValue( + jest.spyOn(MSC4108SignInWithQR.prototype, "negotiateProtocols").mockResolvedValue({}); + jest.spyOn(MSC4108SignInWithQR.prototype, "deviceAuthorizationGrant").mockRejectedValue( new HTTPError("Internal Server Error", 500), ); const fn = jest.spyOn(MSC4108SignInWithQR.prototype, "cancel"); @@ -391,8 +391,8 @@ describe("", () => { jest.spyOn(global.window, "open"); render(getComponent({ client })); - jest.spyOn(MSC4108SignInWithQR.prototype, "loginStep1").mockResolvedValue({}); - jest.spyOn(MSC4108SignInWithQR.prototype, "loginStep2And3").mockResolvedValue({ + jest.spyOn(MSC4108SignInWithQR.prototype, "negotiateProtocols").mockResolvedValue({}); + jest.spyOn(MSC4108SignInWithQR.prototype, "deviceAuthorizationGrant").mockResolvedValue({ verificationUri: "mock-verification-uri", }); @@ -417,8 +417,8 @@ describe("", () => { test("handles errors during reciprocation", async () => { render(getComponent({ client })); - jest.spyOn(MSC4108SignInWithQR.prototype, "loginStep1").mockResolvedValue({}); - jest.spyOn(MSC4108SignInWithQR.prototype, "loginStep2And3").mockResolvedValue({}); + jest.spyOn(MSC4108SignInWithQR.prototype, "negotiateProtocols").mockResolvedValue({}); + jest.spyOn(MSC4108SignInWithQR.prototype, "deviceAuthorizationGrant").mockResolvedValue({}); await waitFor(() => expect(mockedFlow).toHaveBeenLastCalledWith({ phase: Phase.OutOfBandConfirmation, @@ -426,7 +426,7 @@ describe("", () => { }), ); - jest.spyOn(MSC4108SignInWithQR.prototype, "loginStep5").mockRejectedValue( + jest.spyOn(MSC4108SignInWithQR.prototype, "shareSecrets").mockRejectedValue( new HTTPError("Internal Server Error", 500), ); const onClick = mockedFlow.mock.calls[0][0].onClick; @@ -444,9 +444,9 @@ describe("", () => { test("handles user cancelling during reciprocation", async () => { render(getComponent({ client })); - jest.spyOn(MSC4108SignInWithQR.prototype, "loginStep1").mockResolvedValue({}); - jest.spyOn(MSC4108SignInWithQR.prototype, "loginStep2And3").mockResolvedValue({}); - jest.spyOn(MSC4108SignInWithQR.prototype, "loginStep2And3").mockResolvedValue({}); + jest.spyOn(MSC4108SignInWithQR.prototype, "negotiateProtocols").mockResolvedValue({}); + jest.spyOn(MSC4108SignInWithQR.prototype, "deviceAuthorizationGrant").mockResolvedValue({}); + jest.spyOn(MSC4108SignInWithQR.prototype, "deviceAuthorizationGrant").mockResolvedValue({}); await waitFor(() => expect(mockedFlow).toHaveBeenLastCalledWith({ phase: Phase.OutOfBandConfirmation, From a736e67f361db15d99bcd5cc0d6ef55bb8ed560a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 25 Apr 2024 17:47:18 +0100 Subject: [PATCH 65/98] XXX: Add `Link new device` to User Menu Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- res/css/structures/_UserMenu.pcss | 4 ++ src/accessibility/context_menu/MenuItem.tsx | 13 +---- src/components/structures/MatrixChat.tsx | 2 +- src/components/structures/UserMenu.tsx | 53 ++++++++++++++++++- .../views/dialogs/UserSettingsDialog.tsx | 3 +- .../views/elements/AccessibleButton.tsx | 7 ++- .../settings/devices/LoginWithQRSection.tsx | 31 +++++++---- .../settings/tabs/user/SessionManagerTab.tsx | 25 +++++---- src/dispatcher/payloads/OpenToTabPayload.ts | 5 ++ src/i18n/strings/en_EN.json | 3 ++ 10 files changed, 112 insertions(+), 34 deletions(-) diff --git a/res/css/structures/_UserMenu.pcss b/res/css/structures/_UserMenu.pcss index f25c15e48e6..5f8a6a70a1b 100644 --- a/res/css/structures/_UserMenu.pcss +++ b/res/css/structures/_UserMenu.pcss @@ -207,6 +207,10 @@ limitations under the License. .mx_UserMenu_iconSignOut::before { mask-image: url("$(res)/img/element-icons/leave.svg"); } + + .mx_UserMenu_iconQr::before { + mask-image: url("@vector-im/compound-design-tokens/icons/qr-code.svg"); + } } .mx_UserMenu_CustomStatusSection { diff --git a/src/accessibility/context_menu/MenuItem.tsx b/src/accessibility/context_menu/MenuItem.tsx index a244602a091..db3c64468bc 100644 --- a/src/accessibility/context_menu/MenuItem.tsx +++ b/src/accessibility/context_menu/MenuItem.tsx @@ -18,25 +18,16 @@ limitations under the License. import React from "react"; -import { RovingAccessibleButton, RovingAccessibleTooltipButton } from "../RovingTabIndex"; +import { RovingAccessibleButton } from "../RovingTabIndex"; interface IProps extends React.ComponentProps { label?: string; - tooltip?: string; } // Semantic component for representing a role=menuitem -export const MenuItem: React.FC = ({ children, label, tooltip, ...props }) => { +export const MenuItem: React.FC = ({ children, label, ...props }) => { const ariaLabel = props["aria-label"] || label; - if (tooltip) { - return ( - - {children} - - ); - } - return ( {children} diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 25741562791..834c8d4d49a 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -765,7 +765,7 @@ export default class MatrixChat extends React.PureComponent { const tabPayload = payload as OpenToTabPayload; Modal.createDialog( UserSettingsDialog, - { initialTabId: tabPayload.initialTabId as UserTab, sdkContext: this.stores }, + { ...payload.props, initialTabId: tabPayload.initialTabId as UserTab, sdkContext: this.stores }, /*className=*/ undefined, /*isPriority=*/ false, /*isStatic=*/ true, diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index f24fa57d7d8..a7088f9f037 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -52,6 +52,7 @@ import { Icon as LiveIcon } from "../../../res/img/compound/live-8px.svg"; import { VoiceBroadcastRecording, VoiceBroadcastRecordingsStoreEvent } from "../../voice-broadcast"; import { SDKContext } from "../../contexts/SDKContext"; import { shouldShowFeedback } from "../../utils/Feedback"; +import { shouldShowQr } from "../views/settings/devices/LoginWithQRSection"; interface IProps { isPanelCollapsed: boolean; @@ -66,6 +67,7 @@ interface IState { isHighContrast: boolean; selectedSpace?: Room | null; showLiveAvatarAddon: boolean; + supportsQrLogin: boolean; } const toRightOf = (rect: PartialDOMRect): MenuProps => { @@ -103,6 +105,7 @@ export default class UserMenu extends React.Component { isHighContrast: this.isUserOnHighContrastTheme(), selectedSpace: SpaceStore.instance.activeSpaceRoom, showLiveAvatarAddon: this.context.voiceBroadcastRecordingsStore.hasCurrent(), + supportsQrLogin: false, }; OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate); @@ -126,6 +129,7 @@ export default class UserMenu extends React.Component { ); this.dispatcherRef = defaultDispatcher.register(this.onAction); this.themeWatcherRef = SettingsStore.watchSetting("theme", null, this.onThemeChanged); + this.checkQrLoginSupport(); } public componentWillUnmount(): void { @@ -140,6 +144,26 @@ export default class UserMenu extends React.Component { ); } + private checkQrLoginSupport = async (): Promise => { + const oidcClientConfig = SdkConfig.get().validated_server_config?.delegatedAuthentication; + if (oidcClientConfig && this.context.client) { + const [versions, wellKnown, isCrossSigningReady] = await Promise.all([ + this.context.client.getVersions(), + this.context.client.waitForClientWellKnown(), + this.context.client.getCrypto()?.isCrossSigningReady(), + ]); + + const supportsQrLogin = shouldShowQr( + this.context.client, + !!isCrossSigningReady, + oidcClientConfig, + versions, + wellKnown, + ); + this.setState({ supportsQrLogin }); + } + }; + private isUserOnDarkTheme(): boolean { if (SettingsStore.getValue("use_system_theme")) { return window.matchMedia("(prefers-color-scheme: dark)").matches; @@ -237,11 +261,11 @@ export default class UserMenu extends React.Component { SettingsStore.setValue("theme", null, SettingLevel.DEVICE, newTheme); // set at same level as Appearance tab }; - private onSettingsOpen = (ev: ButtonEvent, tabId?: string): void => { + private onSettingsOpen = (ev: ButtonEvent, tabId?: string, props?: Record): void => { ev.preventDefault(); ev.stopPropagation(); - const payload: OpenToTabPayload = { action: Action.ViewUserSettings, initialTabId: tabId }; + const payload: OpenToTabPayload = { action: Action.ViewUserSettings, initialTabId: tabId, props }; defaultDispatcher.dispatch(payload); this.setState({ contextMenuPosition: null }); // also close the menu }; @@ -363,9 +387,34 @@ export default class UserMenu extends React.Component { ); } + const oidcClientConfig = SdkConfig.get().validated_server_config?.delegatedAuthentication; + let linkNewDeviceButton: JSX.Element | undefined; + if (oidcClientConfig) { + const extraProps: Omit< + React.ComponentProps, + "iconClassname" | "label" | "onClick" + > = {}; + if (!this.state.supportsQrLogin) { + extraProps.disabled = true; + extraProps.title = _t("user_menu|link_new_device_not_supported"); + extraProps.caption = _t("user_menu|link_new_device_not_supported_caption"); + extraProps.placement = "right"; + } + + linkNewDeviceButton = ( + this.onSettingsOpen(e, UserTab.SessionManager, { showQrCode: true })} + /> + ); + } + let primaryOptionList = ( {homeButton} + {linkNewDeviceButton} UserTab.SessionManager, _td("settings|sessions|title"), "mx_UserSettingsDialog_sessionsIcon", - , + , // don't track with posthog while under construction undefined, ), diff --git a/src/components/views/elements/AccessibleButton.tsx b/src/components/views/elements/AccessibleButton.tsx index 26b4390e56b..3f6dec701e2 100644 --- a/src/components/views/elements/AccessibleButton.tsx +++ b/src/components/views/elements/AccessibleButton.tsx @@ -96,6 +96,10 @@ type Props = DynamicHtmlElementProps & * Only valid when used in conjunction with `title`. */ caption?: string; + /** + * The placement of the tooltip. + */ + placement?: React.ComponentProps["placement"]; }; /** @@ -128,6 +132,7 @@ const AccessibleButton = forwardRef(function , ref: Ref, @@ -199,7 +204,7 @@ const AccessibleButton = forwardRef(function + {button} ); diff --git a/src/components/views/settings/devices/LoginWithQRSection.tsx b/src/components/views/settings/devices/LoginWithQRSection.tsx index 138edbc3db1..a5065da7671 100644 --- a/src/components/views/settings/devices/LoginWithQRSection.tsx +++ b/src/components/views/settings/devices/LoginWithQRSection.tsx @@ -23,6 +23,7 @@ import { IClientWellKnown, OidcClientConfig, MatrixClient, + DEVICE_CODE_SCOPE, } from "matrix-js-sdk/src/matrix"; import { Icon as QrCodeIcon } from "@vector-im/compound-design-tokens/icons/qr-code.svg"; @@ -39,9 +40,14 @@ interface IProps { capabilities?: Capabilities; wellKnown?: IClientWellKnown; oidcClientConfig?: OidcClientConfig; + isCrossSigningReady?: boolean; } -function showQrLegacy(versions?: IServerVersions, wellKnown?: IClientWellKnown, capabilities?: Capabilities): boolean { +function shouldShowQrLegacy( + versions?: IServerVersions, + wellKnown?: IClientWellKnown, + capabilities?: Capabilities, +): boolean { // Needs server support for (get_login_token or OIDC Device Authorization Grant) and MSC3886: // in r0 of MSC3882 it is exposed as a feature flag, but in stable and unstable r1 it is a capability const loginTokenCapability = GET_LOGIN_TOKEN_CAPABILITY.findIn(capabilities); @@ -52,8 +58,9 @@ function showQrLegacy(versions?: IServerVersions, wellKnown?: IClientWellKnown, return getLoginTokenSupported && msc3886Supported; } -function showQr( +export function shouldShowQr( cli: MatrixClient, + isCrossSigningReady: boolean, oidcClientConfig?: OidcClientConfig, versions?: IServerVersions, wellKnown?: IClientWellKnown, @@ -61,24 +68,30 @@ function showQr( const msc4108Supported = !!versions?.unstable_features?.["org.matrix.msc4108"] || !!wellKnown?.["io.element.rendezvous"]?.server; - const deviceAuthorizationGrantSupported = oidcClientConfig?.metadata?.grant_types_supported.includes( - "urn:ietf:params:oauth:grant-type:device_code", - ); + const deviceAuthorizationGrantSupported = + oidcClientConfig?.metadata?.grant_types_supported.includes(DEVICE_CODE_SCOPE); return ( deviceAuthorizationGrantSupported && msc4108Supported && SettingsStore.getValue(Features.OidcNativeFlow) && cli.getCrypto()?.supportsSecretsForQrLogin() && - cli.getCrypto()?.isCrossSigningReady() + isCrossSigningReady ); } -const LoginWithQRSection: React.FC = ({ onShowQr, versions, capabilities, wellKnown, oidcClientConfig }) => { +const LoginWithQRSection: React.FC = ({ + onShowQr, + versions, + capabilities, + wellKnown, + oidcClientConfig, + isCrossSigningReady, +}) => { const cli = useMatrixClientContext(); const offerShowQr = oidcClientConfig - ? showQr(cli, oidcClientConfig, versions, wellKnown) - : showQrLegacy(versions, wellKnown, capabilities); + ? shouldShowQr(cli, !!isCrossSigningReady, oidcClientConfig, versions, wellKnown) + : shouldShowQrLegacy(versions, wellKnown, capabilities); // don't show anything if no method is available if (!offerShowQr) { diff --git a/src/components/views/settings/tabs/user/SessionManagerTab.tsx b/src/components/views/settings/tabs/user/SessionManagerTab.tsx index 85df2540b39..12149a16875 100644 --- a/src/components/views/settings/tabs/user/SessionManagerTab.tsx +++ b/src/components/views/settings/tabs/user/SessionManagerTab.tsx @@ -151,7 +151,9 @@ const useSignOut = ( }; }; -const SessionManagerTab: React.FC = () => { +const SessionManagerTab: React.FC<{ + showQrCode?: boolean; +}> = ({ showQrCode }) => { const { devices, dehydratedDeviceId, @@ -195,6 +197,10 @@ const SessionManagerTab: React.FC = () => { return discoverAndValidateOIDCIssuerWellKnown(authIssuer.issuer); } }, [matrixClient]); + const isCrossSigningReady = useAsyncMemo( + async () => matrixClient.getCrypto()?.isCrossSigningReady() ?? false, + [matrixClient], + ); const onDeviceExpandToggle = (deviceId: ExtendedDevice["device_id"]): void => { if (expandedDeviceIds.includes(deviceId)) { @@ -277,7 +283,7 @@ const SessionManagerTab: React.FC = () => { } : undefined; - const [signInWithQrMode, setSignInWithQrMode] = useState(); + const [signInWithQrMode, setSignInWithQrMode] = useState(showQrCode ? Mode.Show : null); const onQrFinish = useCallback(() => { setSignInWithQrMode(null); @@ -303,6 +309,14 @@ const SessionManagerTab: React.FC = () => { return ( + { /> )} - ); diff --git a/src/dispatcher/payloads/OpenToTabPayload.ts b/src/dispatcher/payloads/OpenToTabPayload.ts index 7548c342e40..fb1986afaa6 100644 --- a/src/dispatcher/payloads/OpenToTabPayload.ts +++ b/src/dispatcher/payloads/OpenToTabPayload.ts @@ -24,4 +24,9 @@ export interface OpenToTabPayload extends ActionPayload { * The tab ID to open in the settings view to start, if possible. */ initialTabId?: string; + + /** + * Additional properties to pass to the settings view. + */ + props?: Record; } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index f6072cf2a7b..430ceee945e 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -3786,6 +3786,9 @@ "verify_explainer": "For extra security, verify this user by checking a one-time code on both of your devices." }, "user_menu": { + "link_new_device": "Link new device", + "link_new_device_not_supported": "Not supported", + "link_new_device_not_supported_caption": "You need to sign in manually", "settings": "All settings", "switch_theme_dark": "Switch to dark mode", "switch_theme_light": "Switch to light mode" From b4f8cacc80c3d79159b3909283bd82b99c529069 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 25 Apr 2024 18:26:32 +0100 Subject: [PATCH 66/98] XXX iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../views/dialogs/UserSettingsDialog.tsx | 2 +- src/components/views/rooms/RoomList.tsx | 8 +++--- src/components/views/rooms/RoomListHeader.tsx | 4 +-- .../CurrentDeviceSection-test.tsx.snap | 28 ++++++++----------- 4 files changed, 19 insertions(+), 23 deletions(-) diff --git a/src/components/views/dialogs/UserSettingsDialog.tsx b/src/components/views/dialogs/UserSettingsDialog.tsx index f4ed1f2f4ea..b0bc31fadbe 100644 --- a/src/components/views/dialogs/UserSettingsDialog.tsx +++ b/src/components/views/dialogs/UserSettingsDialog.tsx @@ -40,7 +40,7 @@ import { SDKContext, SdkContextClass } from "../../../contexts/SDKContext"; interface IProps { initialTabId?: UserTab; - showQrCode: boolean; + showQrCode?: boolean; sdkContext: SdkContextClass; onFinished(): void; } diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx index 073950e30f4..d573f3bbf02 100644 --- a/src/components/views/rooms/RoomList.tsx +++ b/src/components/views/rooms/RoomList.tsx @@ -160,7 +160,7 @@ const DmAuxButton: React.FC = ({ tabIndex, dispatcher = default showSpaceInvite(activeSpace); }} disabled={!canInvite} - tooltip={canInvite ? undefined : _t("spaces|error_no_permission_invite")} + title={canInvite ? undefined : _t("spaces|error_no_permission_invite")} /> )} @@ -250,7 +250,7 @@ const UntaggedAuxButton: React.FC = ({ tabIndex }) => { PosthogTrackers.trackInteraction("WebRoomListRoomsSublistPlusMenuCreateRoomItem", e); }} disabled={!canAddRooms} - tooltip={canAddRooms ? undefined : _t("spaces|error_no_permission_create_room")} + title={canAddRooms ? undefined : _t("spaces|error_no_permission_create_room")} /> {videoRoomsEnabled && ( = ({ tabIndex }) => { ); }} disabled={!canAddRooms} - tooltip={canAddRooms ? undefined : _t("spaces|error_no_permission_create_room")} + title={canAddRooms ? undefined : _t("spaces|error_no_permission_create_room")} > @@ -281,7 +281,7 @@ const UntaggedAuxButton: React.FC = ({ tabIndex }) => { showAddExistingRooms(activeSpace); }} disabled={!canAddRooms} - tooltip={canAddRooms ? undefined : _t("spaces|error_no_permission_add_room")} + title={canAddRooms ? undefined : _t("spaces|error_no_permission_add_room")} /> ) : null} diff --git a/src/components/views/rooms/RoomListHeader.tsx b/src/components/views/rooms/RoomListHeader.tsx index 690300dfa28..bcd918eaf38 100644 --- a/src/components/views/rooms/RoomListHeader.tsx +++ b/src/components/views/rooms/RoomListHeader.tsx @@ -267,7 +267,7 @@ const RoomListHeader: React.FC = ({ onVisibilityChange }) => { closePlusMenu(); }} disabled={!canAddSubRooms} - tooltip={!canAddSubRooms ? _t("spaces|error_no_permission_add_room") : undefined} + title={!canAddSubRooms ? _t("spaces|error_no_permission_add_room") : undefined} /> {canCreateSpaces && ( = ({ onVisibilityChange }) => { closePlusMenu(); }} disabled={!canAddSubSpaces} - tooltip={!canAddSubSpaces ? _t("spaces|error_no_permission_add_space") : undefined} + title={!canAddSubSpaces ? _t("spaces|error_no_permission_add_space") : undefined} > diff --git a/test/components/views/settings/devices/__snapshots__/CurrentDeviceSection-test.tsx.snap b/test/components/views/settings/devices/__snapshots__/CurrentDeviceSection-test.tsx.snap index 2cd642eac92..bd3ec55ca66 100644 --- a/test/components/views/settings/devices/__snapshots__/CurrentDeviceSection-test.tsx.snap +++ b/test/components/views/settings/devices/__snapshots__/CurrentDeviceSection-test.tsx.snap @@ -135,26 +135,22 @@ exports[` handles when device is falsy 1`] = ` > Current session -
Date: Thu, 25 Apr 2024 18:26:46 +0100 Subject: [PATCH 67/98] Don't use validated_server_config as that is only set for OIDC-native Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/UserMenu.tsx | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index a7088f9f037..e25879cb7a2 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -15,7 +15,7 @@ limitations under the License. */ import React, { createRef, ReactNode } from "react"; -import { Room } from "matrix-js-sdk/src/matrix"; +import { discoverAndValidateOIDCIssuerWellKnown, Room } from "matrix-js-sdk/src/matrix"; import { MatrixClientPeg } from "../../MatrixClientPeg"; import defaultDispatcher from "../../dispatcher/dispatcher"; @@ -67,6 +67,7 @@ interface IState { isHighContrast: boolean; selectedSpace?: Room | null; showLiveAvatarAddon: boolean; + showQrLogin: boolean; supportsQrLogin: boolean; } @@ -105,6 +106,7 @@ export default class UserMenu extends React.Component { isHighContrast: this.isUserOnHighContrastTheme(), selectedSpace: SpaceStore.instance.activeSpaceRoom, showLiveAvatarAddon: this.context.voiceBroadcastRecordingsStore.hasCurrent(), + showQrLogin: false, supportsQrLogin: false, }; @@ -145,9 +147,12 @@ export default class UserMenu extends React.Component { } private checkQrLoginSupport = async (): Promise => { - const oidcClientConfig = SdkConfig.get().validated_server_config?.delegatedAuthentication; - if (oidcClientConfig && this.context.client) { - const [versions, wellKnown, isCrossSigningReady] = await Promise.all([ + if (!this.context.client) return; + + const { issuer } = await this.context.client.getAuthIssuer(); + if (issuer) { + const [oidcClientConfig, versions, wellKnown, isCrossSigningReady] = await Promise.all([ + discoverAndValidateOIDCIssuerWellKnown(issuer), this.context.client.getVersions(), this.context.client.waitForClientWellKnown(), this.context.client.getCrypto()?.isCrossSigningReady(), @@ -160,7 +165,7 @@ export default class UserMenu extends React.Component { versions, wellKnown, ); - this.setState({ supportsQrLogin }); + this.setState({ supportsQrLogin, showQrLogin: true }); } }; @@ -387,9 +392,8 @@ export default class UserMenu extends React.Component { ); } - const oidcClientConfig = SdkConfig.get().validated_server_config?.delegatedAuthentication; let linkNewDeviceButton: JSX.Element | undefined; - if (oidcClientConfig) { + if (this.state.showQrLogin) { const extraProps: Omit< React.ComponentProps, "iconClassname" | "label" | "onClick" From f6f64e8558b8dff0ffab558b9c9c303f04895351 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 26 Apr 2024 09:34:10 +0100 Subject: [PATCH 68/98] Fix tests Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/UserMenu.tsx | 2 +- test/test-utils/client.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index e25879cb7a2..06fb92eacbd 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -149,7 +149,7 @@ export default class UserMenu extends React.Component { private checkQrLoginSupport = async (): Promise => { if (!this.context.client) return; - const { issuer } = await this.context.client.getAuthIssuer(); + const { issuer } = await this.context.client.getAuthIssuer().catch(() => ({ issuer: undefined })); if (issuer) { const [oidcClientConfig, versions, wellKnown, isCrossSigningReady] = await Promise.all([ discoverAndValidateOIDCIssuerWellKnown(issuer), diff --git a/test/test-utils/client.ts b/test/test-utils/client.ts index 8a991b0e9cc..b5df1e976a1 100644 --- a/test/test-utils/client.ts +++ b/test/test-utils/client.ts @@ -17,7 +17,7 @@ limitations under the License. import EventEmitter from "events"; import { MethodLikeKeys, mocked, MockedObject, PropertyLikeKeys } from "jest-mock"; import { Feature, ServerSupport } from "matrix-js-sdk/src/feature"; -import { MatrixClient, User } from "matrix-js-sdk/src/matrix"; +import { MatrixClient, MatrixError, User } from "matrix-js-sdk/src/matrix"; import { MatrixClientPeg } from "../../src/MatrixClientPeg"; @@ -135,6 +135,7 @@ export const mockClientMethodsServer = (): Partial Date: Fri, 26 Apr 2024 12:50:01 +0100 Subject: [PATCH 69/98] Fix "Link New Device" action Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/UserMenu.tsx | 2 +- src/components/views/dialogs/UserSettingsDialog.tsx | 4 ++-- .../views/settings/tabs/user/SessionManagerTab.tsx | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index 06fb92eacbd..27a94a8b435 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -410,7 +410,7 @@ export default class UserMenu extends React.Component { {...extraProps} iconClassName="mx_UserMenu_iconQr" label={_t("user_menu|link_new_device")} - onClick={(e) => this.onSettingsOpen(e, UserTab.SessionManager, { showQrCode: true })} + onClick={(e) => this.onSettingsOpen(e, UserTab.SessionManager, { showMsc4108QrCode: true })} /> ); } diff --git a/src/components/views/dialogs/UserSettingsDialog.tsx b/src/components/views/dialogs/UserSettingsDialog.tsx index b0bc31fadbe..a965d1aa638 100644 --- a/src/components/views/dialogs/UserSettingsDialog.tsx +++ b/src/components/views/dialogs/UserSettingsDialog.tsx @@ -40,7 +40,7 @@ import { SDKContext, SdkContextClass } from "../../../contexts/SDKContext"; interface IProps { initialTabId?: UserTab; - showQrCode?: boolean; + showMsc4108QrCode?: boolean; sdkContext: SdkContextClass; onFinished(): void; } @@ -90,7 +90,7 @@ export default class UserSettingsDialog extends React.Component UserTab.SessionManager, _td("settings|sessions|title"), "mx_UserSettingsDialog_sessionsIcon", - , + , // don't track with posthog while under construction undefined, ), diff --git a/src/components/views/settings/tabs/user/SessionManagerTab.tsx b/src/components/views/settings/tabs/user/SessionManagerTab.tsx index 12149a16875..a8c27a3fd47 100644 --- a/src/components/views/settings/tabs/user/SessionManagerTab.tsx +++ b/src/components/views/settings/tabs/user/SessionManagerTab.tsx @@ -152,8 +152,8 @@ const useSignOut = ( }; const SessionManagerTab: React.FC<{ - showQrCode?: boolean; -}> = ({ showQrCode }) => { + showMsc4108QrCode?: boolean; +}> = ({ showMsc4108QrCode }) => { const { devices, dehydratedDeviceId, @@ -283,7 +283,7 @@ const SessionManagerTab: React.FC<{ } : undefined; - const [signInWithQrMode, setSignInWithQrMode] = useState(showQrCode ? Mode.Show : null); + const [signInWithQrMode, setSignInWithQrMode] = useState(showMsc4108QrCode ? Mode.Show : null); const onQrFinish = useCallback(() => { setSignInWithQrMode(null); @@ -300,7 +300,7 @@ const SessionManagerTab: React.FC<{ mode={signInWithQrMode} onFinished={onQrFinish} client={matrixClient} - legacy={!oidcClientConfig} + legacy={!oidcClientConfig && !showMsc4108QrCode} /> ); From bfce4520058178c7186682198bc3f76f27664348 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 26 Apr 2024 12:50:08 +0100 Subject: [PATCH 70/98] i18n Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/i18n/strings/en_EN.json | 62 ++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index b0c521226d5..202e6f0188f 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -246,41 +246,41 @@ "phone_optional_label": "Phone (optional)", "qr_code_login": { "approve_access_warning": "By approving access for this device, it will have full access to your account.", + "check_code_explainer": "Confirm that you see a green checkmark on the other device to verify that the connection is secure.", + "check_code_heading": "Do you see a green checkmark on your other device?", + "check_code_input_label": "2-digit code", + "check_code_mismatch": "The numbers don't match", "completing_setup": "Completing set up of your new device", "confirm_code_match": "Check that the code below matches with your other device:", + "error_etag_missing": "An unexpected error occurred. This may be due to a browser extension, proxy server, or server misconfiguration.", + "error_expired": "Sign in expired. Please try again.", + "error_expired_title": "The sign in was not completed in time", + "error_insecure_channel_detected": "A secure connection could not be made to the new device. Your existing devices are still safe and you don't need to worry about them.", + "error_insecure_channel_detected_instructions": "Now what?", + "error_insecure_channel_detected_instructions_1": "Try signing in to the other device again with a QR code in case this was a network problem", + "error_insecure_channel_detected_instructions_2": "If you encounter the same problem, try a different wifi network or use your mobile data instead of wifi", + "error_insecure_channel_detected_instructions_3": "If that doesn't work, sign in manually", + "error_insecure_channel_detected_title": "Connection not secure", + "error_other_device_already_signed_in": "You don’t need to do anything else.", + "error_other_device_already_signed_in_title": "Your other device is already signed in", "error_rate_limited": "Too many attempts in a short time. Wait some time before trying again.", "error_unexpected": "An unexpected error occurred. The request to connect your other device has been cancelled.", + "error_unsupported_protocol": "This device does not support signing in to the other device with a QR code.", + "error_unsupported_protocol_title": "Other device not compatible", + "error_user_cancelled": "The sign in was cancelled on the other device.", + "error_user_cancelled_title": "Sign in request cancelled", + "error_user_declined": "You declined the request from your other device to sign in.", + "error_user_declined_title": "Sign in declined", "follow_remaining_instructions": "Follow the remaining instructions to verify your other device", "open_element_other_device": "Open %(brand)s on your other device", "point_the_camera": "Point the camera at the QR code shown here", "scan_code_instruction": "Scan the QR code with another device", "scan_qr_code": "Sign in with QR code", + "security_code": "Security code", + "security_code_prompt": "If asked, enter the code below on your other device.", "select_qr_code": "Select \"%(scanQRCode)s\"", "sign_in_new_device": "Sign in new device", - "waiting_for_device": "Waiting for device to sign in", - "security_code_prompt": "If asked, enter the code below on your other device.", - "security_code": "Security code", - "error_user_declined_title": "Sign in declined", - "error_user_declined": "You declined the request from your other device to sign in.", - "error_user_cancelled_title": "Sign in request cancelled", - "error_user_cancelled": "The sign in was cancelled on the other device.", - "error_unsupported_protocol_title": "Other device not compatible", - "error_unsupported_protocol": "This device does not support signing in to the other device with a QR code.", - "error_other_device_already_signed_in_title": "Your other device is already signed in", - "error_other_device_already_signed_in": "You don’t need to do anything else.", - "error_insecure_channel_detected_title": "Connection not secure", - "error_insecure_channel_detected_instructions_3": "If that doesn't work, sign in manually", - "error_insecure_channel_detected_instructions_2": "If you encounter the same problem, try a different wifi network or use your mobile data instead of wifi", - "error_insecure_channel_detected_instructions_1": "Try signing in to the other device again with a QR code in case this was a network problem", - "error_insecure_channel_detected_instructions": "Now what?", - "error_insecure_channel_detected": "A secure connection could not be made to the new device. Your existing devices are still safe and you don't need to worry about them.", - "error_expired_title": "The sign in was not completed in time", - "error_expired": "Sign in expired. Please try again.", - "error_etag_missing": "An unexpected error occurred. This may be due to a browser extension, proxy server, or server misconfiguration.", - "check_code_mismatch": "The numbers don't match", - "check_code_input_label": "2-digit code", - "check_code_heading": "Do you see a green checkmark on your other device?", - "check_code_explainer": "Confirm that you see a green checkmark on the other device to verify that the connection is secure." + "waiting_for_device": "Waiting for device to sign in" }, "register_action": "Create Account", "registration": { @@ -3228,9 +3228,9 @@ "creation_summary_room": "%(creator)s created and configured the room.", "decryption_failure": { "blocked": "The sender has blocked you from receiving this message", - "unable_to_decrypt": "Unable to decrypt message", + "historical_event_no_key_backup": "Historical messages are not available on this device", "historical_event_unverified_device": "You need to verify this device for access to historical messages", - "historical_event_no_key_backup": "Historical messages are not available on this device" + "unable_to_decrypt": "Unable to decrypt message" }, "disambiguated_profile": "%(displayName)s (%(matrixId)s)", "download_action_decrypting": "Decrypting", @@ -3788,12 +3788,12 @@ "verify_explainer": "For extra security, verify this user by checking a one-time code on both of your devices." }, "user_menu": { + "link_new_device": "Link new device", + "link_new_device_not_supported": "Not supported", + "link_new_device_not_supported_caption": "You need to sign in manually", "settings": "All settings", "switch_theme_dark": "Switch to dark mode", - "switch_theme_light": "Switch to light mode", - "link_new_device_not_supported_caption": "You need to sign in manually", - "link_new_device_not_supported": "Not supported", - "link_new_device": "Link new device" + "switch_theme_light": "Switch to light mode" }, "voice_broadcast": { "30s_backward": "30s backward", @@ -4087,4 +4087,4 @@ "wordByItself": "A word by itself is easy to guess" } } -} \ No newline at end of file +} From 48f5e67baa2cb495c3e923e6f9077295c2d59e56 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 26 Apr 2024 13:03:56 +0100 Subject: [PATCH 71/98] Clear show qr code state when navigating tabs Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/dialogs/UserSettingsDialog.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/components/views/dialogs/UserSettingsDialog.tsx b/src/components/views/dialogs/UserSettingsDialog.tsx index a965d1aa638..861e245226f 100644 --- a/src/components/views/dialogs/UserSettingsDialog.tsx +++ b/src/components/views/dialogs/UserSettingsDialog.tsx @@ -47,6 +47,7 @@ interface IProps { interface IState { mjolnirEnabled: boolean; + showMsc4108QrCode?: boolean; // store it in state as changing tabs back and forth should clear it } export default class UserSettingsDialog extends React.Component { @@ -57,6 +58,7 @@ export default class UserSettingsDialog extends React.Component this.state = { mjolnirEnabled: SettingsStore.getValue("feature_mjolnir"), + showMsc4108QrCode: props.showMsc4108QrCode, }; } @@ -73,6 +75,13 @@ export default class UserSettingsDialog extends React.Component this.setState({ mjolnirEnabled: newValue }); }; + private onTabChanged = (): void => { + this.setState({ + // Clear this so switching away from the tab and back to it will not show the QR code again + showMsc4108QrCode: false, + }); + }; + private getTabs(): NonEmptyArray> { const tabs: Tab[] = []; @@ -90,7 +99,7 @@ export default class UserSettingsDialog extends React.Component UserTab.SessionManager, _td("settings|sessions|title"), "mx_UserSettingsDialog_sessionsIcon", - , + , // don't track with posthog while under construction undefined, ), @@ -215,6 +224,7 @@ export default class UserSettingsDialog extends React.Component tabs={this.getTabs()} initialTabId={this.props.initialTabId} screenName="UserSettings" + onChange={this.onTabChanged} />
From d6400eefb7fd5743eebd72bebf9dc037e227bc14 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 26 Apr 2024 13:12:32 +0100 Subject: [PATCH 72/98] Ensure the channel is not cancelled when the flow is concluded Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/auth/LoginWithQR.tsx | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index 522cf6e28eb..0df53949540 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -81,6 +81,8 @@ export type FailureReason = RendezvousFailureReason | LoginWithQRFailureReason; * This uses the unstable feature of MSC3906: https://github.com/matrix-org/matrix-spec-proposals/pull/3906 */ export default class LoginWithQR extends React.Component { + private finished = false; + public constructor(props: IProps) { super(props); @@ -118,7 +120,7 @@ export default class LoginWithQR extends React.Component { } public componentWillUnmount(): void { - if (this.state.rendezvous) { + if (this.state.rendezvous && !this.finished) { // eslint-disable-next-line react/no-direct-mutation-state this.state.rendezvous.onFailure = undefined; // calling cancel will call close() as well to clean up the resources @@ -156,7 +158,7 @@ export default class LoginWithQR extends React.Component { } if (!this.props.client.getCrypto()) { // no E2EE to set up - this.props.onFinished(true); + this.onFinished(true); return; } this.setState({ phase: Phase.Verifying }); @@ -167,7 +169,7 @@ export default class LoginWithQR extends React.Component { } finally { this.setState({ rendezvous: undefined }); } - this.props.onFinished(true); + this.onFinished(true); } catch (e) { logger.error("Error whilst approving sign in", e); if (e instanceof HTTPError && e.httpStatus === 429) { @@ -179,6 +181,11 @@ export default class LoginWithQR extends React.Component { } } + private onFinished(success: boolean): void { + this.finished = true; + this.props.onFinished(success); + } + private generateAndShowCode = async (): Promise => { let rendezvous: MSC4108SignInWithQR | MSC3906Rendezvous; try { @@ -286,7 +293,7 @@ export default class LoginWithQR extends React.Component { await this.state.rendezvous.shareSecrets(); // done - this.props.onFinished(true); + this.onFinished(true); } else { this.setState({ phase: Phase.Error, failureReason: ClientRendezvousFailureReason.Unknown }); throw new Error("New device flows around OIDC are not yet implemented"); @@ -326,7 +333,7 @@ export default class LoginWithQR extends React.Component { await this.state.rendezvous?.cancel(MSC4108FailureReason.UserCancelled); } this.reset(); - this.props.onFinished(false); + this.onFinished(false); break; case Click.Approve: await (this.props.legacy ? this.legacyApproveLogin() : this.approveLogin(checkCode)); @@ -334,7 +341,7 @@ export default class LoginWithQR extends React.Component { case Click.Decline: await this.state.rendezvous?.declineLoginOnExistingDevice(); this.reset(); - this.props.onFinished(false); + this.onFinished(false); break; case Click.Back: if (this.state.rendezvous instanceof MSC3906Rendezvous) { @@ -342,7 +349,7 @@ export default class LoginWithQR extends React.Component { } else { await this.state.rendezvous?.cancel(MSC4108FailureReason.UserCancelled); } - this.props.onFinished(false); + this.onFinished(false); break; case Click.ShowQr: await this.updateMode(Mode.Show); From f6bfa5db98b5b61deb7364cd11b6a6aec2647993 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 26 Apr 2024 14:28:47 +0100 Subject: [PATCH 73/98] Improve coverage Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- test/components/structures/UserMenu-test.tsx | 54 +++++++++++++++++++- test/test-utils/test-utils.ts | 1 + 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/test/components/structures/UserMenu-test.tsx b/test/components/structures/UserMenu-test.tsx index 22addc5a359..b19c18cab38 100644 --- a/test/components/structures/UserMenu-test.tsx +++ b/test/components/structures/UserMenu-test.tsx @@ -16,8 +16,9 @@ limitations under the License. import React from "react"; import { act, render, RenderResult, screen, waitFor } from "@testing-library/react"; -import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; +import { CryptoApi, DEVICE_CODE_SCOPE, MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; import { mocked } from "jest-mock"; +import fetchMock from "fetch-mock-jest"; import UnwrappedUserMenu from "../../../src/components/structures/UserMenu"; import { stubClient, wrapInSdkContext } from "../../test-utils"; @@ -31,6 +32,12 @@ import { TestSdkContext } from "../../TestSdkContext"; import defaultDispatcher from "../../../src/dispatcher/dispatcher"; import LogoutDialog from "../../../src/components/views/dialogs/LogoutDialog"; import Modal from "../../../src/Modal"; +import SettingsStore from "../../../src/settings/SettingsStore"; +import { Features } from "../../../src/settings/Settings"; +import { SettingLevel } from "../../../src/settings/SettingLevel"; +import { mockOpenIdConfiguration } from "../../test-utils/oidc"; +import { Action } from "../../../src/dispatcher/actions"; +import { UserTab } from "../../../src/components/views/dialogs/UserTab"; describe("", () => { let client: MatrixClient; @@ -39,6 +46,7 @@ describe("", () => { beforeEach(() => { sdkContext = new TestSdkContext(); + jest.restoreAllMocks(); }); describe(" when video broadcast", () => { @@ -177,4 +185,48 @@ describe("", () => { }); }); }); + + it("should render 'Link new device' button in OIDC native mode", async () => { + sdkContext.client = stubClient(); + mocked(sdkContext.client.getAuthIssuer).mockResolvedValue({ issuer: "https://issuer/" }); + const openIdMetadata = mockOpenIdConfiguration("https://issuer/"); + openIdMetadata.grant_types_supported.push(DEVICE_CODE_SCOPE); + fetchMock.get("https://issuer/.well-known/openid-configuration", openIdMetadata); + fetchMock.get("https://issuer/jwks", { + status: 200, + headers: { + "Content-Type": "application/json", + }, + keys: [], + }); + mocked(sdkContext.client.getVersions).mockResolvedValue({ + versions: [], + unstable_features: { + "org.matrix.msc4108": true, + }, + }); + mocked(sdkContext.client.waitForClientWellKnown).mockResolvedValue({}); + mocked(sdkContext.client.getCrypto).mockReturnValue({ + isCrossSigningReady: jest.fn().mockResolvedValue(true), + supportsSecretsForQrLogin: jest.fn().mockResolvedValue(true), + } as unknown as CryptoApi); + await SettingsStore.setValue(Features.OidcNativeFlow, null, SettingLevel.DEVICE, true); + const spy = jest.spyOn(defaultDispatcher, "dispatch"); + + const UserMenu = wrapInSdkContext(UnwrappedUserMenu, sdkContext); + render(); + + screen.getByRole("button", { name: /User menu/i }).click(); + await expect(screen.findByText("Link new device")).resolves.toBeInTheDocument(); + + // Assert the QR code is shown directly + screen.getByRole("menuitem", { name: "Link new device" }).click(); + await waitFor(() => { + expect(spy).toHaveBeenCalledWith({ + action: Action.ViewUserSettings, + initialTabId: UserTab.SessionManager, + props: { showMsc4108QrCode: true }, + }); + }); + }); }); diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts index b124af4ad11..f59c86f781c 100644 --- a/test/test-utils/test-utils.ts +++ b/test/test-utils/test-utils.ts @@ -271,6 +271,7 @@ export function createTestClient(): MatrixClient { getMediaConfig: jest.fn(), baseUrl: "https://matrix-client.matrix.org", matrixRTC: createStubMatrixRTC(), + getAuthIssuer: jest.fn(), } as unknown as MatrixClient; client.reEmitter = new ReEmitter(client); From ecb1bc34c305cb980dc3fc0f00d30d195ab4b8b7 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 26 Apr 2024 14:48:10 +0100 Subject: [PATCH 74/98] Fix test Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- test/components/structures/UserMenu-test.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/test/components/structures/UserMenu-test.tsx b/test/components/structures/UserMenu-test.tsx index b19c18cab38..d38d43c8b06 100644 --- a/test/components/structures/UserMenu-test.tsx +++ b/test/components/structures/UserMenu-test.tsx @@ -46,7 +46,6 @@ describe("", () => { beforeEach(() => { sdkContext = new TestSdkContext(); - jest.restoreAllMocks(); }); describe(" when video broadcast", () => { From 7ee252c6d0d3434476ad497fbac8240c47329223 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 29 Apr 2024 10:47:16 +0100 Subject: [PATCH 75/98] Fix styling bug Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- res/css/views/auth/_LoginWithQR.pcss | 4 ---- 1 file changed, 4 deletions(-) diff --git a/res/css/views/auth/_LoginWithQR.pcss b/res/css/views/auth/_LoginWithQR.pcss index 0c79f08071f..a020b969b27 100644 --- a/res/css/views/auth/_LoginWithQR.pcss +++ b/res/css/views/auth/_LoginWithQR.pcss @@ -36,10 +36,6 @@ limitations under the License. } .mx_UserSettingsDialog .mx_LoginWithQR { - .mx_AccessibleButton + .mx_AccessibleButton { - margin-left: $spacing-12; - } - font: var(--cpd-font-body-md-regular); h1 { From c306b355c32cc82c9fee49c2a569aaf39b59dda3 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 29 Apr 2024 10:48:26 +0100 Subject: [PATCH 76/98] Update copy Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/i18n/strings/en_EN.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 202e6f0188f..3a4e57ec992 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -246,8 +246,8 @@ "phone_optional_label": "Phone (optional)", "qr_code_login": { "approve_access_warning": "By approving access for this device, it will have full access to your account.", - "check_code_explainer": "Confirm that you see a green checkmark on the other device to verify that the connection is secure.", - "check_code_heading": "Do you see a green checkmark on your other device?", + "check_code_explainer": "This will verify that the connection to your other device is secure.", + "check_code_heading": "Enter the number shown on your other device", "check_code_input_label": "2-digit code", "check_code_mismatch": "The numbers don't match", "completing_setup": "Completing set up of your new device", From 79ad9a239310edbeed3851415bc18384764f1a84 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 29 Apr 2024 11:04:45 +0100 Subject: [PATCH 77/98] Update snapshot Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../devices/__snapshots__/LoginWithQRFlow-test.tsx.snap | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/components/views/settings/devices/__snapshots__/LoginWithQRFlow-test.tsx.snap b/test/components/views/settings/devices/__snapshots__/LoginWithQRFlow-test.tsx.snap index 54c263db73f..e8fba37ea29 100644 --- a/test/components/views/settings/devices/__snapshots__/LoginWithQRFlow-test.tsx.snap +++ b/test/components/views/settings/devices/__snapshots__/LoginWithQRFlow-test.tsx.snap @@ -917,12 +917,12 @@ exports[` renders check code confirmation 1`] = `

- Do you see a green checkmark on your other device? + Enter the number shown on your other device

- Confirm that you see a green checkmark on the other device to verify that the connection is secure. + This will verify that the connection to your other device is secure.

From 2e8f4ecfda1df4f0f33cdb0a355550054711bf93 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 1 May 2024 10:01:22 +0100 Subject: [PATCH 80/98] Remove redundant (for now) scanning code Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/Lifecycle.ts | 9 +++-- src/MatrixClientPeg.ts | 70 +++++++----------------------------- test/MatrixClientPeg-test.ts | 20 ----------- 3 files changed, 17 insertions(+), 82 deletions(-) diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts index c05a2deb533..61097c13c21 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -22,7 +22,6 @@ import { createClient, MatrixClient, SSOAction, OidcTokenRefresher } from "matri import { IEncryptedPayload } from "matrix-js-sdk/src/crypto/aes"; import { QueryDict } from "matrix-js-sdk/src/utils"; import { logger } from "matrix-js-sdk/src/logger"; -import { QRSecretsBundle } from "matrix-js-sdk/src/crypto-api"; import { IMatrixClientCreds, MatrixClientPeg } from "./MatrixClientPeg"; import { ModuleRunner } from "./modules/ModuleRunner"; @@ -821,7 +820,7 @@ async function doSetLoggedIn(credentials: IMatrixClientCreds, clearStorageEnable checkSessionLock(); dis.fire(Action.OnLoggedIn); - await startMatrixClient(client, /*startSyncing=*/ !softLogout, credentials.secrets); + await startMatrixClient(client, /*startSyncing=*/ !softLogout); return client; } @@ -959,7 +958,7 @@ export function isLoggingOut(): boolean { * @param {boolean} startSyncing True (default) to actually start * syncing the client. */ -async function startMatrixClient(client: MatrixClient, startSyncing = true, secrets?: QRSecretsBundle): Promise { +async function startMatrixClient(client: MatrixClient, startSyncing = true): Promise { logger.log(`Lifecycle: Starting MatrixClient`); // dispatch this before starting the matrix client: it's used @@ -990,10 +989,10 @@ async function startMatrixClient(client: MatrixClient, startSyncing = true, secr // index (e.g. the FilePanel), therefore initialize the event index // before the client. await EventIndexPeg.init(); - await MatrixClientPeg.start(secrets); + await MatrixClientPeg.start(); } else { logger.warn("Caller requested only auxiliary services be started"); - await MatrixClientPeg.assign(secrets); + await MatrixClientPeg.assign(); } checkSessionLock(); diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index 2518f478d59..f19fa0c1625 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -32,7 +32,6 @@ import { import { VerificationMethod } from "matrix-js-sdk/src/types"; import * as utils from "matrix-js-sdk/src/utils"; import { logger } from "matrix-js-sdk/src/logger"; -import { QRSecretsBundle } from "matrix-js-sdk/src/crypto-api"; import createMatrixClient from "./utils/createMatrixClient"; import SettingsStore from "./settings/SettingsStore"; @@ -65,7 +64,6 @@ export interface IMatrixClientCreds { guest?: boolean; pickleKey?: string; freshLogin?: boolean; - secrets?: QRSecretsBundle; } /** @@ -75,46 +73,22 @@ export interface IMatrixClientCreds { * you'll find a `MatrixClient` hanging on the `MatrixClientPeg`. */ export interface IMatrixClientPeg { - /** - * The opts used to start the client - */ opts: IStartClientOpts; /** * Return the server name of the user's homeserver * Throws an error if unable to deduce the homeserver name - * (e.g. if the user is not logged in) + * (eg. if the user is not logged in) * * @returns {string} The homeserver name, if present. */ getHomeserverName(): string; - /** - * Get the current MatrixClient, if any - */ get(): MatrixClient | null; - - /** - * Get the current MatrixClient, throwing an error if there isn't one - */ safeGet(): MatrixClient; - - /** - * Unset the current MatrixClient - */ unset(): void; - - /** - * Prepare the MatrixClient for use, including initialising the store and crypto, but do not start it - * @param secrets the secrets to use if authenticating using QR login - */ - assign(secrets?: QRSecretsBundle): Promise; - - /** - * Prepare the MatrixClient for use, including initialising the store and crypto, and start it - * @param secrets the secrets to use if authenticating using QR login - */ - start(secrets?: QRSecretsBundle): Promise; + assign(): Promise; + start(): Promise; /** * If we've registered a user ID we set this to the ID of the @@ -136,13 +110,13 @@ export interface IMatrixClientPeg { /** * If the current user has been registered by this device then this - * returns boolean of whether it was within the last N hours given. + * returns a boolean of whether it was within the last N hours given. */ userRegisteredWithinLastHours(hours: number): boolean; /** * If the current user has been registered by this device then this - * returns boolean of whether it was after a given timestamp. + * returns a boolean of whether it was after a given timestamp. */ userRegisteredAfter(date: Date): boolean; @@ -261,7 +235,7 @@ class MatrixClientPegClass implements IMatrixClientPeg { PlatformPeg.get()?.reload(); }; - public async assign(secrets?: QRSecretsBundle): Promise { + public async assign(): Promise { if (!this.matrixClient) { throw new Error("createClient must be called first"); } @@ -288,7 +262,7 @@ class MatrixClientPegClass implements IMatrixClientPeg { // try to initialise e2e on the new client if (!SettingsStore.getValue("lowBandwidth")) { - await this.initClientCrypto(secrets); + await this.initClientCrypto(); } const opts = utils.deepCopy(this.opts); @@ -299,17 +273,9 @@ class MatrixClientPegClass implements IMatrixClientPeg { opts.threadSupport = true; if (SettingsStore.getValue("feature_sliding_sync")) { - const proxyUrl = SettingsStore.getValue("feature_sliding_sync_proxy_url"); - if (proxyUrl) { - logger.log("Activating sliding sync using proxy at ", proxyUrl); - } else { - logger.log("Activating sliding sync"); - } - opts.slidingSync = SlidingSyncManager.instance.configure( - this.matrixClient, - proxyUrl || this.matrixClient.baseUrl, - ); - SlidingSyncManager.instance.startSpidering(100, 50); // 100 rooms at a time, 50ms apart + opts.slidingSync = await SlidingSyncManager.instance.setup(this.matrixClient); + } else { + SlidingSyncManager.instance.checkSupport(this.matrixClient); } // Connect the matrix client to the dispatcher and setting handlers @@ -323,7 +289,7 @@ class MatrixClientPegClass implements IMatrixClientPeg { /** * Attempt to initialize the crypto layer on a newly-created MatrixClient */ - private async initClientCrypto(secrets?: QRSecretsBundle): Promise { + private async initClientCrypto(): Promise { if (!this.matrixClient) { throw new Error("createClient must be called first"); } @@ -361,16 +327,6 @@ class MatrixClientPegClass implements IMatrixClientPeg { if (useRustCrypto) { await this.matrixClient.initRustCrypto(); - if (secrets) { - this.matrixClient.getCrypto()?.importSecretsForQrLogin(secrets); - if (secrets.backup) { - const backupInfo = await this.matrixClient.getKeyBackupVersion(); - if (backupInfo) { - await this.matrixClient.restoreKeyBackupWithCache(undefined, undefined, backupInfo, {}); - } - } - } - StorageManager.setCryptoInitialised(true); // TODO: device dehydration and whathaveyou return; @@ -398,8 +354,8 @@ class MatrixClientPegClass implements IMatrixClientPeg { } } - public async start(secrets?: QRSecretsBundle): Promise { - const opts = await this.assign(secrets); + public async start(): Promise { + const opts = await this.assign(); logger.log(`MatrixClientPeg: really starting MatrixClient`); await this.matrixClient!.startClient(opts); diff --git a/test/MatrixClientPeg-test.ts b/test/MatrixClientPeg-test.ts index 6349111aa25..eeb79d4263a 100644 --- a/test/MatrixClientPeg-test.ts +++ b/test/MatrixClientPeg-test.ts @@ -279,26 +279,6 @@ describe("MatrixClientPeg", () => { expect(mockSetValue).toHaveBeenCalledWith("feature_rust_crypto", null, SettingLevel.DEVICE, false); }); - it("should import qr login secrets when passed", async () => { - fetchMockJest.get("http://example.com/_matrix/client/v3/room_keys/version", { - status: 404, - body: { errcode: "M_NOT_FOUND" }, - }); - const importSecretsForQrLogin = jest.fn(); - jest.spyOn(RustCrypto, "initRustCrypto").mockResolvedValue({ - importSecretsForQrLogin, - setSupportedVerificationMethods: jest.fn(), - onRoomMembership: jest.fn(), - on: jest.fn(), - } as any); - const secrets = { - cross_signing: { master_key: "mk", user_signing_key: "usk", self_signing_key: "ssk" }, - backup: { algorithm: "m.megolm_backup.v1.curve25519-aes-sha2", backup_version: "1", key: "key" }, - }; - await testPeg.start(secrets); - expect(importSecretsForQrLogin).toHaveBeenCalledWith(secrets); - }); - describe("Rust staged rollout", () => { function mockSettingStore( userIsUsingRust: boolean, From a213f2ca8e88175e490d746baf6daa6ef6fa1509 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 1 May 2024 10:07:29 +0100 Subject: [PATCH 81/98] delint Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- test/MatrixClientPeg-test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/MatrixClientPeg-test.ts b/test/MatrixClientPeg-test.ts index eeb79d4263a..2ed08e0a21f 100644 --- a/test/MatrixClientPeg-test.ts +++ b/test/MatrixClientPeg-test.ts @@ -21,8 +21,6 @@ import { ProvideCryptoSetupExtensions, SecretStorageKeyDescription, } from "@matrix-org/react-sdk-module-api/lib/lifecycles/CryptoSetupExtensions"; -// eslint-disable-next-line no-restricted-imports -import * as RustCrypto from "matrix-js-sdk/src/rust-crypto"; import { advanceDateAndTime, stubClient } from "./test-utils"; import { IMatrixClientPeg, MatrixClientPeg as peg } from "../src/MatrixClientPeg"; From 9b6fcac3130a2a4a480de23ce43d49a90b420ada Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 1 May 2024 10:22:04 +0100 Subject: [PATCH 82/98] Cull more early code Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/auth/LoginWithQR.tsx | 27 ++++++----------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index 0df53949540..26694706916 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -39,7 +39,7 @@ import { wrapRequestWithDialog } from "../../../utils/UserInteractiveAuth"; import { _t } from "../../../languageHandler"; interface IProps { - client?: MatrixClient; + client: MatrixClient; mode: Mode; legacy: boolean; onFinished(...args: any): void; @@ -59,7 +59,6 @@ interface IState { checkCode?: string; failureReason?: FailureReason; lastScannedCode?: Buffer; - ourIntent: RendezvousIntent; homeserverBaseUrl?: string; } @@ -88,12 +87,13 @@ export default class LoginWithQR extends React.Component { this.state = { phase: Phase.Loading, - ourIntent: this.props.client - ? RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE - : RendezvousIntent.LOGIN_ON_NEW_DEVICE, }; } + private get ourIntent(): RendezvousIntent { + return RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE; + } + public componentDidMount(): void { this.updateMode(this.props.mode).then(() => {}); } @@ -226,20 +226,7 @@ export default class LoginWithQR extends React.Component { if (rendezvous instanceof MSC3906Rendezvous) { const confirmationDigits = await rendezvous.startAfterShowingCode(); this.setState({ phase: Phase.LegacyConnected, confirmationDigits }); - } else if (this.state.ourIntent === RendezvousIntent.LOGIN_ON_NEW_DEVICE) { - // MSC4108-Flow: ExistingScanned - - // we get the homserver URL from the secure channel, but we don't trust it yet - const { homeserverBaseUrl } = await rendezvous.negotiateProtocols(); - - if (!homeserverBaseUrl) { - throw new Error("We don't know the homeserver"); - } - this.setState({ - phase: Phase.OutOfBandConfirmation, - homeserverBaseUrl, - }); - } else { + } else if (this.ourIntent === RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE) { // MSC4108-Flow: NewScanned await rendezvous.negotiateProtocols(); const { verificationUri } = await rendezvous.deviceAuthorizationGrant(); @@ -279,7 +266,7 @@ export default class LoginWithQR extends React.Component { } try { - if (this.state.ourIntent === RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE) { + if (this.ourIntent === RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE) { // MSC4108-Flow: NewScanned this.setState({ phase: Phase.Loading }); From 6998deadef3d2348fbef022d2b410c69e6bbb371 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 1 May 2024 10:58:13 +0100 Subject: [PATCH 83/98] Tidy Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/auth/LoginWithQR.tsx | 4 ++-- src/components/views/auth/LoginWithQRFlow.tsx | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index 26694706916..756103d9440 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -194,11 +194,11 @@ export default class LoginWithQR extends React.Component { if (this.props.legacy) { const transport = new MSC3886SimpleHttpRendezvousTransport({ onFailure: this.onFailure, - client: this.props.client!, + client: this.props.client, fallbackRzServer, }); const channel = new MSC3903ECDHv2RendezvousChannel(transport, undefined, this.onFailure); - rendezvous = new MSC3906Rendezvous(channel, this.props.client!, this.onFailure); + rendezvous = new MSC3906Rendezvous(channel, this.props.client, this.onFailure); } else { const transport = new MSC4108RendezvousSession({ onFailure: this.onFailure, diff --git a/src/components/views/auth/LoginWithQRFlow.tsx b/src/components/views/auth/LoginWithQRFlow.tsx index 6825cb0b465..a69c2fc4dd3 100644 --- a/src/components/views/auth/LoginWithQRFlow.tsx +++ b/src/components/views/auth/LoginWithQRFlow.tsx @@ -104,7 +104,9 @@ export default class LoginWithQRFlow extends React.Component Date: Wed, 1 May 2024 13:08:50 +0100 Subject: [PATCH 84/98] Fix error handling Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/auth/LoginWithQRFlow.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/views/auth/LoginWithQRFlow.tsx b/src/components/views/auth/LoginWithQRFlow.tsx index a69c2fc4dd3..2d4ee888918 100644 --- a/src/components/views/auth/LoginWithQRFlow.tsx +++ b/src/components/views/auth/LoginWithQRFlow.tsx @@ -104,9 +104,7 @@ export default class LoginWithQRFlow extends React.Component Date: Tue, 7 May 2024 17:03:19 +0100 Subject: [PATCH 85/98] Bump @matrix-org/matrix-sdk-crypto-wasm to 90b63b84df65c19161f94049d83218cb4dfff97d Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- yarn.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index 60bbaa23cb6..8a69bf28dde 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1899,7 +1899,7 @@ "@matrix-org/matrix-sdk-crypto-wasm@^4.9.0": version "4.9.0" - resolved "https://codeload.github.com/matrix-org/matrix-rust-sdk-crypto-wasm/tar.gz/78c5fb5cc29979d0bd188a8b0ec163dd30aa7936" + resolved "https://codeload.github.com/matrix-org/matrix-rust-sdk-crypto-wasm/tar.gz/90b63b84df65c19161f94049d83218cb4dfff97d" "@matrix-org/matrix-wysiwyg@2.17.0": version "2.17.0" From 88efbc7c0f199643d9a05e9577a8a70c5cd5852c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 7 May 2024 17:10:43 +0100 Subject: [PATCH 86/98] i18n Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/i18n/strings/en_EN.json | 80 ++++++++++++++++++------------------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 9c2d4b3ce0e..4322ad5b85c 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -246,8 +246,13 @@ "phone_optional_label": "Phone (optional)", "qr_code_login": { "approve_access_warning": "By approving access for this device, it will have full access to your account.", + "check_code_explainer": "This will verify that the connection to your other device is secure.", + "check_code_heading": "Enter the number shown on your other device", + "check_code_input_label": "2-digit code", + "check_code_mismatch": "The numbers don't match", "completing_setup": "Completing set up of your new device", "confirm_code_match": "Check that the code below matches with your other device:", + "error_etag_missing": "An unexpected error occurred. This may be due to a browser extension, proxy server, or server misconfiguration.", "error_expired": "Sign in expired. Please try again.", "error_expired_title": "The sign in was not completed in time", "error_insecure_channel_detected": "A secure connection could not be made to the new device. Your existing devices are still safe and you don't need to worry about them.", @@ -271,16 +276,11 @@ "point_the_camera": "Point the camera at the QR code shown here", "scan_code_instruction": "Scan the QR code with another device", "scan_qr_code": "Sign in with QR code", + "security_code": "Security code", + "security_code_prompt": "If asked, enter the code below on your other device.", "select_qr_code": "Select \"%(scanQRCode)s\"", "sign_in_new_device": "Sign in new device", - "waiting_for_device": "Waiting for device to sign in", - "security_code_prompt": "If asked, enter the code below on your other device.", - "security_code": "Security code", - "error_etag_missing": "An unexpected error occurred. This may be due to a browser extension, proxy server, or server misconfiguration.", - "check_code_mismatch": "The numbers don't match", - "check_code_input_label": "2-digit code", - "check_code_heading": "Enter the number shown on your other device", - "check_code_explainer": "This will verify that the connection to your other device is secure." + "waiting_for_device": "Waiting for device to sign in" }, "register_action": "Create Account", "registration": { @@ -2401,12 +2401,12 @@ "brand_version": "%(brand)s version:", "clear_cache_reload": "Clear cache and reload", "crypto_version": "Crypto version:", + "dialog_title": "Settings: Help & About", "help_link": "For help with using %(brand)s, click here.", "homeserver": "Homeserver is %(homeserverUrl)s", "identity_server": "Identity server is %(identityServerUrl)s", "title": "Help & About", - "versions": "Versions", - "dialog_title": "Settings: Help & About" + "versions": "Versions" } }, "settings": { @@ -2424,6 +2424,7 @@ "custom_theme_invalid": "Invalid theme schema.", "custom_theme_success": "Theme added!", "custom_theme_url": "Custom theme URL", + "dialog_title": "Settings: Appearance", "font_size": "Font size", "font_size_default": "%(fontSize)s (default)", "image_size_default": "Default", @@ -2432,8 +2433,7 @@ "layout_irc": "IRC (Experimental)", "match_system_theme": "Match system theme", "timeline_image_size": "Image size in the timeline", - "use_high_contrast": "Use high contrast", - "dialog_title": "Settings: Appearance" + "use_high_contrast": "Use high contrast" }, "automatic_language_detection_syntax_highlight": "Enable automatic language detection for syntax highlighting", "autoplay_gifs": "Autoplay GIFs", @@ -2473,6 +2473,7 @@ "deactivate_confirm_erase_label": "Hide my messages from new joiners", "deactivate_section": "Deactivate Account", "deactivate_warning": "Deactivating your account is a permanent action — be careful!", + "dialog_title": "Settings: General", "discovery_email_empty": "Discovery options will appear once you have added an email above.", "discovery_email_verification_instructions": "Verify the link in your inbox", "discovery_msisdn_empty": "Discovery options will appear once you have added a phone number above.", @@ -2519,8 +2520,7 @@ "remove_email_prompt": "Remove %(email)s?", "remove_msisdn_prompt": "Remove %(phone)s?", "spell_check_locale_placeholder": "Choose a locale", - "spell_check_section": "Spell check", - "dialog_title": "Settings: General" + "spell_check_section": "Spell check" }, "image_thumbnails": "Show previews/thumbnails for images", "inline_url_previews_default": "Enable inline URL previews by default", @@ -2581,13 +2581,20 @@ "phrase_strong_enough": "Great! This passphrase looks strong enough" }, "keyboard": { - "title": "Keyboard", - "dialog_title": "Settings: Keyboard" + "dialog_title": "Settings: Keyboard", + "title": "Keyboard" + }, + "labs": { + "dialog_title": "Settings: Labs" + }, + "labs_mjolnir": { + "dialog_title": "Settings: Ignored Users" }, "notifications": { "default_setting_description": "This setting will be applied by default to all your rooms.", "default_setting_section": "I want to be notified for (Default Setting)", "desktop_notification_message_preview": "Show message preview in desktop notification", + "dialog_title": "Settings: Notifications", "email_description": "Receive an email summary of missed notifications", "email_section": "Email summary", "email_select": "Select which emails you want to send summaries to. Manage your emails in .", @@ -2637,8 +2644,7 @@ "rule_suppress_notices": "Messages sent by bot", "rule_tombstone": "When rooms are upgraded", "show_message_desktop_notification": "Show message in desktop notification", - "voip": "Audio and Video calls", - "dialog_title": "Settings: Notifications" + "voip": "Audio and Video calls" }, "preferences": { "Electron.enableHardwareAcceleration": "Enable hardware acceleration (restart %(appName)s to take effect)", @@ -2647,6 +2653,7 @@ "code_blocks_heading": "Code blocks", "compact_modern": "Use a more compact 'Modern' layout", "composer_heading": "Composer", + "dialog_title": "Settings: Preferences", "enable_hardware_acceleration": "Enable hardware acceleration", "enable_tray_icon": "Show tray icon and minimise window to it on close", "keyboard_heading": "Keyboard shortcuts", @@ -2661,8 +2668,7 @@ "show_checklist_shortcuts": "Show shortcut to welcome checklist above the room list", "show_polls_button": "Show polls button", "surround_text": "Surround selected text when typing special characters", - "time_heading": "Displaying time", - "dialog_title": "Settings: Preferences" + "time_heading": "Displaying time" }, "prompt_invite": "Prompt before sending invites to potentially invalid matrix IDs", "replace_plain_emoji": "Automatically replace plain text Emoji", @@ -2697,6 +2703,7 @@ "dehydrated_device_enabled": "Offline device enabled", "delete_backup": "Delete Backup", "delete_backup_confirm_description": "Are you sure? You will lose your encrypted messages if your keys are not backed up properly.", + "dialog_title": "Settings: Security & Privacy", "e2ee_default_disabled_warning": "Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.", "enable_message_search": "Enable message search in encrypted rooms", "encryption_individual_verification_mode": "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.", @@ -2745,8 +2752,7 @@ "send_analytics": "Send analytics data", "session_id": "Session ID:", "session_key": "Session key:", - "strict_encryption": "Never send encrypted messages to unverified sessions from this session", - "dialog_title": "Settings: Security & Privacy" + "strict_encryption": "Never send encrypted messages to unverified sessions from this session" }, "send_read_receipts": "Send read receipts", "send_read_receipts_unsupported": "Your server doesn't support disabling sending read receipts.", @@ -2777,6 +2783,7 @@ "device_unverified_description_current": "Verify your current session for enhanced secure messaging.", "device_verified_description": "This session is ready for secure messaging.", "device_verified_description_current": "Your current session is ready for secure messaging.", + "dialog_title": "Settings: Sessions", "error_pusher_state": "Failed to set pusher state", "error_set_name": "Failed to set session name", "filter_all": "All", @@ -2846,8 +2853,7 @@ "verified_sessions_explainer_2": "This means that you have all the keys needed to unlock your encrypted messages and confirm to other users that you trust this session.", "verified_sessions_list_description": "For best security, sign out from any session that you don't recognize or use anymore.", "verify_session": "Verify session", - "web_session": "Web session", - "dialog_title": "Settings: Sessions" + "web_session": "Web session" }, "show_avatar_changes": "Show profile picture changes", "show_breadcrumbs": "Show shortcuts to recently viewed rooms above the room list", @@ -2861,6 +2867,7 @@ "show_typing_notifications": "Show typing notifications", "showbold": "Show all activity in the room list (dots or number of unread messages)", "sidebar": { + "dialog_title": "Settings: Sidebar", "metaspaces_favourites_description": "Group all your favourite rooms and people in one place.", "metaspaces_home_all_rooms": "Show all rooms", "metaspaces_home_all_rooms_description": "Show all your rooms in Home, even if they're in a space.", @@ -2873,8 +2880,7 @@ "metaspaces_video_rooms_description": "Group all private video rooms and conferences.", "metaspaces_video_rooms_description_invite_extension": "In conferences you can invite people outside of matrix.", "spaces_explainer": "Spaces are ways to group rooms and people. Alongside the spaces you're in, you can use some pre-built ones too.", - "title": "Sidebar", - "dialog_title": "Settings: Sidebar" + "title": "Sidebar" }, "start_automatically": "Start automatically after system login", "tac_only_notifications": "Only show notifications in the thread activity centre", @@ -2891,6 +2897,7 @@ "audio_output_empty": "No Audio Outputs detected", "auto_gain_control": "Automatic gain control", "connection_section": "Connection", + "dialog_title": "Settings: Voice & Video", "echo_cancellation": "Echo cancellation", "enable_fallback_ice_server": "Allow fallback call assist server (%(server)s)", "enable_fallback_ice_server_description": "Only applies if your homeserver does not offer one. Your IP address would be shared during a call.", @@ -2903,17 +2910,10 @@ "video_section": "Video settings", "voice_agc": "Automatically adjust the microphone volume", "voice_processing": "Voice processing", - "voice_section": "Voice settings", - "dialog_title": "Settings: Voice & Video" + "voice_section": "Voice settings" }, "warn_quit": "Warn before quitting", - "warning": "WARNING: ", - "labs_mjolnir": { - "dialog_title": "Settings: Ignored Users" - }, - "labs": { - "dialog_title": "Settings: Labs" - } + "warning": "WARNING: " }, "share": { "link_title": "Link to room", @@ -3794,12 +3794,12 @@ "verify_explainer": "For extra security, verify this user by checking a one-time code on both of your devices." }, "user_menu": { + "link_new_device": "Link new device", + "link_new_device_not_supported": "Not supported", + "link_new_device_not_supported_caption": "You need to sign in manually", "settings": "All settings", "switch_theme_dark": "Switch to dark mode", - "switch_theme_light": "Switch to light mode", - "link_new_device_not_supported_caption": "You need to sign in manually", - "link_new_device_not_supported": "Not supported", - "link_new_device": "Link new device" + "switch_theme_light": "Switch to light mode" }, "voice_broadcast": { "30s_backward": "30s backward", @@ -4093,4 +4093,4 @@ "wordByItself": "A word by itself is easy to guess" } } -} \ No newline at end of file +} From 57cc27df6742d95cccec49e6abc143ee393a594f Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 9 May 2024 15:02:26 +0100 Subject: [PATCH 87/98] Tweak copy Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/i18n/strings/en_EN.json | 4 ++-- .../devices/__snapshots__/LoginWithQRFlow-test.tsx.snap | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 4322ad5b85c..8f29ba2b38a 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -271,9 +271,9 @@ "error_user_cancelled_title": "Sign in request cancelled", "error_user_declined": "You declined the request from your other device to sign in.", "error_user_declined_title": "Sign in declined", - "follow_remaining_instructions": "Follow the instructions to link your other device", + "follow_remaining_instructions": "Follow the remaining instructions", "open_element_other_device": "Open %(brand)s on your other device", - "point_the_camera": "Point the camera at the QR code shown here", + "point_the_camera": "Scan the QR code shown here", "scan_code_instruction": "Scan the QR code with another device", "scan_qr_code": "Sign in with QR code", "security_code": "Security code", diff --git a/test/components/views/settings/devices/__snapshots__/LoginWithQRFlow-test.tsx.snap b/test/components/views/settings/devices/__snapshots__/LoginWithQRFlow-test.tsx.snap index 2a021cff58e..df003454bf3 100644 --- a/test/components/views/settings/devices/__snapshots__/LoginWithQRFlow-test.tsx.snap +++ b/test/components/views/settings/devices/__snapshots__/LoginWithQRFlow-test.tsx.snap @@ -890,10 +890,10 @@ exports[` renders QR code 1`] = `
  • - Point the camera at the QR code shown here + Scan the QR code shown here
  • - Follow the instructions to link your other device + Follow the remaining instructions
  • From 8129f507f8d32d37c9538ffbdf7e52f62664b8d6 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 23 May 2024 13:08:26 +0100 Subject: [PATCH 88/98] Discard changes to yarn.lock --- yarn.lock | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index 94aa9a847e2..52ddc95a42d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1914,7 +1914,8 @@ "@matrix-org/matrix-sdk-crypto-wasm@^4.9.0": version "4.9.0" - resolved "https://codeload.github.com/matrix-org/matrix-rust-sdk-crypto-wasm/tar.gz/90b63b84df65c19161f94049d83218cb4dfff97d" + resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-4.9.0.tgz#9dfed83e33f760650596c4e5c520e5e4c53355d2" + integrity sha512-/bgA4QfE7qkK6GFr9hnhjAvRSebGrmEJxukU0ukbudZcYvbzymoBBM8j3HeULXZT8kbw8WH6z63txYTMCBSDOA== "@matrix-org/matrix-wysiwyg@2.17.0": version "2.17.0" From c5d61a2304c55399e305da1478b88f9975dd90d1 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 23 May 2024 13:22:47 +0100 Subject: [PATCH 89/98] i18n Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/i18n/strings/en_EN.json | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 03793227162..267b6225335 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -246,8 +246,13 @@ "phone_optional_label": "Phone (optional)", "qr_code_login": { "approve_access_warning": "By approving access for this device, it will have full access to your account.", + "check_code_explainer": "This will verify that the connection to your other device is secure.", + "check_code_heading": "Enter the number shown on your other device", + "check_code_input_label": "2-digit code", + "check_code_mismatch": "The numbers don't match", "completing_setup": "Completing set up of your new device", "confirm_code_match": "Check that the code below matches with your other device:", + "error_etag_missing": "An unexpected error occurred. This may be due to a browser extension, proxy server, or server misconfiguration.", "error_expired": "Sign in expired. Please try again.", "error_expired_title": "The sign in was not completed in time", "error_insecure_channel_detected": "A secure connection could not be made to the new device. Your existing devices are still safe and you don't need to worry about them.", @@ -271,16 +276,11 @@ "point_the_camera": "Scan the QR code shown here", "scan_code_instruction": "Scan the QR code with another device", "scan_qr_code": "Sign in with QR code", + "security_code": "Security code", + "security_code_prompt": "If asked, enter the code below on your other device.", "select_qr_code": "Select \"%(scanQRCode)s\"", "sign_in_new_device": "Sign in new device", - "waiting_for_device": "Waiting for device to sign in", - "security_code_prompt": "If asked, enter the code below on your other device.", - "security_code": "Security code", - "error_etag_missing": "An unexpected error occurred. This may be due to a browser extension, proxy server, or server misconfiguration.", - "check_code_mismatch": "The numbers don't match", - "check_code_input_label": "2-digit code", - "check_code_heading": "Enter the number shown on your other device", - "check_code_explainer": "This will verify that the connection to your other device is secure." + "waiting_for_device": "Waiting for device to sign in" }, "register_action": "Create Account", "registration": { @@ -3794,12 +3794,12 @@ "verify_explainer": "For extra security, verify this user by checking a one-time code on both of your devices." }, "user_menu": { + "link_new_device": "Link new device", + "link_new_device_not_supported": "Not supported", + "link_new_device_not_supported_caption": "You need to sign in manually", "settings": "All settings", "switch_theme_dark": "Switch to dark mode", - "switch_theme_light": "Switch to light mode", - "link_new_device_not_supported_caption": "You need to sign in manually", - "link_new_device_not_supported": "Not supported", - "link_new_device": "Link new device" + "switch_theme_light": "Switch to light mode" }, "voice_broadcast": { "30s_backward": "30s backward", @@ -4093,4 +4093,4 @@ "wordByItself": "A word by itself is easy to guess" } } -} \ No newline at end of file +} From a66d1bf8c75f6e0129a0e77fdd5ffceed9ed8242 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 23 May 2024 16:53:19 +0100 Subject: [PATCH 90/98] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/auth/LoginWithQRFlow.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/auth/LoginWithQRFlow.tsx b/src/components/views/auth/LoginWithQRFlow.tsx index da53b586e96..036dc1b451f 100644 --- a/src/components/views/auth/LoginWithQRFlow.tsx +++ b/src/components/views/auth/LoginWithQRFlow.tsx @@ -1,5 +1,5 @@ /* -Copyright 2022 The Matrix.org Foundation C.I.C. +Copyright 2022 - 2024 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. From 80c31e2557fbddd0695df6a630d61d1e376b0c78 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 23 May 2024 16:59:17 +0100 Subject: [PATCH 91/98] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../views/settings/tabs/user/SessionManagerTab.tsx | 10 +++++++--- test/test-utils/client.ts | 1 + 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/components/views/settings/tabs/user/SessionManagerTab.tsx b/src/components/views/settings/tabs/user/SessionManagerTab.tsx index 0e6606ddb82..ee51d0680fb 100644 --- a/src/components/views/settings/tabs/user/SessionManagerTab.tsx +++ b/src/components/views/settings/tabs/user/SessionManagerTab.tsx @@ -192,9 +192,13 @@ const SessionManagerTab: React.FC<{ const capabilities = useAsyncMemo(async () => matrixClient?.getCapabilities(), [matrixClient]); const wellKnown = useMemo(() => matrixClient?.getClientWellKnown(), [matrixClient]); const oidcClientConfig = useAsyncMemo(async () => { - const authIssuer = await matrixClient?.getAuthIssuer(); - if (authIssuer) { - return discoverAndValidateOIDCIssuerWellKnown(authIssuer.issuer); + try { + const authIssuer = await matrixClient?.getAuthIssuer(); + if (authIssuer) { + return discoverAndValidateOIDCIssuerWellKnown(authIssuer.issuer); + } + } catch (e) { + logger.error("Failed to discover OIDC metadata", e); } }, [matrixClient]); const isCrossSigningReady = useAsyncMemo( diff --git a/test/test-utils/client.ts b/test/test-utils/client.ts index c1b044ff9d9..00f5aa3f7b8 100644 --- a/test/test-utils/client.ts +++ b/test/test-utils/client.ts @@ -180,4 +180,5 @@ export const mockClientMethodsCrypto = (): Partial< export const mockClientMethodsRooms = (rooms: Room[] = []): Partial, unknown>> => ({ getRooms: jest.fn().mockReturnValue(rooms), + getRoom: jest.fn((roomId) => rooms.find((r) => r.roomId === roomId) ?? null), }); From 34223b27e8567ff3e8cf2911ad6ac974eea4b710 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 28 May 2024 13:28:27 +0100 Subject: [PATCH 92/98] Skip checkQrLoginSupport if OIDC labs flag is not enabled Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/UserMenu.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index fdd8c133aff..0e6b17ccc07 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -53,6 +53,7 @@ import { VoiceBroadcastRecording, VoiceBroadcastRecordingsStoreEvent } from "../ import { SDKContext } from "../../contexts/SDKContext"; import { shouldShowFeedback } from "../../utils/Feedback"; import { shouldShowQr } from "../views/settings/devices/LoginWithQRSection"; +import { Features } from "../../settings/Settings"; interface IProps { isPanelCollapsed: boolean; @@ -147,7 +148,7 @@ export default class UserMenu extends React.Component { } private checkQrLoginSupport = async (): Promise => { - if (!this.context.client) return; + if (!this.context.client || !SettingsStore.getValue(Features.OidcNativeFlow)) return; const { issuer } = await this.context.client.getAuthIssuer().catch(() => ({ issuer: undefined })); if (issuer) { From 9f2afe044a1ebd196c71528eee2d8414e3f1ec5e Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Fri, 31 May 2024 09:21:22 +0100 Subject: [PATCH 93/98] Use RendezvousError.code as failure reason during approveLogin() --- src/components/views/auth/LoginWithQR.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index f7b2561d2e6..c837249c3cd 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -291,7 +291,7 @@ export default class LoginWithQR extends React.Component { } } catch (e: RendezvousError | unknown) { logger.error("Error whilst approving sign in", e); - this.setState({ phase: Phase.Error, failureReason: ClientRendezvousFailureReason.Unknown }); + this.setState({ phase: Phase.Error, failureReason: e instanceof RendezvousError ? e.code : ClientRendezvousFailureReason.Unknown }); } }; From fe0d014ad4b4b07c3c87f0b226ff2ecd206af0df Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Fri, 31 May 2024 09:39:53 +0100 Subject: [PATCH 94/98] Lint --- src/components/views/auth/LoginWithQR.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index c837249c3cd..4f97122370b 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -291,7 +291,10 @@ export default class LoginWithQR extends React.Component { } } catch (e: RendezvousError | unknown) { logger.error("Error whilst approving sign in", e); - this.setState({ phase: Phase.Error, failureReason: e instanceof RendezvousError ? e.code : ClientRendezvousFailureReason.Unknown }); + this.setState({ + phase: Phase.Error, + failureReason: e instanceof RendezvousError ? e.code : ClientRendezvousFailureReason.Unknown, + }); } }; From 07e2c07695b1ecf2f71fbc7fae5f304024828314 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 31 May 2024 14:38:22 +0100 Subject: [PATCH 95/98] Update copy Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/i18n/strings/en_EN.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 267b6225335..d7304887713 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -269,7 +269,7 @@ "error_unsupported_protocol_title": "Other device not compatible", "error_user_cancelled": "The sign in was cancelled on the other device.", "error_user_cancelled_title": "Sign in request cancelled", - "error_user_declined": "You declined the request from your other device to sign in.", + "error_user_declined": "You or the account provider declined the sign in request.", "error_user_declined_title": "Sign in declined", "follow_remaining_instructions": "Follow the remaining instructions", "open_element_other_device": "Open %(brand)s on your other device", From 8eb5d1b77301306ad7245890e3f4af3958f5da4c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 31 May 2024 15:03:50 +0100 Subject: [PATCH 96/98] Update snapshot Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../devices/__snapshots__/LoginWithQRFlow-test.tsx.snap | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/components/views/settings/devices/__snapshots__/LoginWithQRFlow-test.tsx.snap b/test/components/views/settings/devices/__snapshots__/LoginWithQRFlow-test.tsx.snap index df003454bf3..56873e510b2 100644 --- a/test/components/views/settings/devices/__snapshots__/LoginWithQRFlow-test.tsx.snap +++ b/test/components/views/settings/devices/__snapshots__/LoginWithQRFlow-test.tsx.snap @@ -785,7 +785,7 @@ exports[` errors renders user_declined 1`] = `

    - You declined the request from your other device to sign in. + You or the account provider declined the sign in request.

    errors renders user_declined 2`] = `

    - You declined the request from your other device to sign in. + You or the account provider declined the sign in request.

    Date: Wed, 5 Jun 2024 11:56:22 +0100 Subject: [PATCH 97/98] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/settings/devices/LoginWithQRSection.tsx | 2 +- test/components/structures/UserMenu-test.tsx | 3 ++- .../views/settings/devices/LoginWithQRSection-test.tsx | 2 +- .../views/settings/tabs/user/SessionManagerTab-test.tsx | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/views/settings/devices/LoginWithQRSection.tsx b/src/components/views/settings/devices/LoginWithQRSection.tsx index a5065da7671..9c7ed9efe68 100644 --- a/src/components/views/settings/devices/LoginWithQRSection.tsx +++ b/src/components/views/settings/devices/LoginWithQRSection.tsx @@ -75,7 +75,7 @@ export function shouldShowQr( deviceAuthorizationGrantSupported && msc4108Supported && SettingsStore.getValue(Features.OidcNativeFlow) && - cli.getCrypto()?.supportsSecretsForQrLogin() && + !!cli.getCrypto()?.exportSecretsBundle && isCrossSigningReady ); } diff --git a/test/components/structures/UserMenu-test.tsx b/test/components/structures/UserMenu-test.tsx index d38d43c8b06..a47e9d48eae 100644 --- a/test/components/structures/UserMenu-test.tsx +++ b/test/components/structures/UserMenu-test.tsx @@ -16,7 +16,8 @@ limitations under the License. import React from "react"; import { act, render, RenderResult, screen, waitFor } from "@testing-library/react"; -import { CryptoApi, DEVICE_CODE_SCOPE, MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; +import { DEVICE_CODE_SCOPE, MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; +import { CryptoApi } from "matrix-js-sdk/src/crypto-api"; import { mocked } from "jest-mock"; import fetchMock from "fetch-mock-jest"; diff --git a/test/components/views/settings/devices/LoginWithQRSection-test.tsx b/test/components/views/settings/devices/LoginWithQRSection-test.tsx index 495116cdfde..027aeed45b0 100644 --- a/test/components/views/settings/devices/LoginWithQRSection-test.tsx +++ b/test/components/views/settings/devices/LoginWithQRSection-test.tsx @@ -149,7 +149,7 @@ describe("", () => { }); test("no support in crypto", async () => { - mocked(client.getCrypto()!.supportsSecretsForQrLogin).mockReturnValue(false); + client.getCrypto()!.exportSecretsBundle = undefined; const { container } = render(getComponent({ client })); expect(container.textContent).toBe(""); // show nothing }); diff --git a/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx b/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx index fc37c887d76..a88c322361c 100644 --- a/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx +++ b/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx @@ -1732,7 +1732,7 @@ describe("", () => { }, }); mockClient.getAuthIssuer.mockResolvedValue({ issuer }); - mockCrypto.supportsSecretsForQrLogin.mockReturnValue(true); + mockCrypto.exportSecretsBundle = jest.fn(); fetchMock.mock(`${issuer}/.well-known/openid-configuration`, { ...openIdConfiguration, grant_types_supported: [ From 5fa8598ad142c0ec67a04abb551b2a0d7dca7c73 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 5 Jun 2024 12:08:57 +0100 Subject: [PATCH 98/98] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- test/components/structures/UserMenu-test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/components/structures/UserMenu-test.tsx b/test/components/structures/UserMenu-test.tsx index a47e9d48eae..24b75a87d15 100644 --- a/test/components/structures/UserMenu-test.tsx +++ b/test/components/structures/UserMenu-test.tsx @@ -208,7 +208,7 @@ describe("", () => { mocked(sdkContext.client.waitForClientWellKnown).mockResolvedValue({}); mocked(sdkContext.client.getCrypto).mockReturnValue({ isCrossSigningReady: jest.fn().mockResolvedValue(true), - supportsSecretsForQrLogin: jest.fn().mockResolvedValue(true), + exportSecretsBundle: jest.fn().mockResolvedValue({}), } as unknown as CryptoApi); await SettingsStore.setValue(Features.OidcNativeFlow, null, SettingLevel.DEVICE, true); const spy = jest.spyOn(defaultDispatcher, "dispatch");