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

Commit

Permalink
MSC4108 support OIDC QR code login (#12370)
Browse files Browse the repository at this point in the history
Co-authored-by: Hugh Nimmo-Smith <[email protected]>
  • Loading branch information
t3chguy and hughns authored Jun 6, 2024
1 parent ca77607 commit 1677ed1
Show file tree
Hide file tree
Showing 24 changed files with 1,548 additions and 723 deletions.
2 changes: 0 additions & 2 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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/transports",
"!matrix-js-sdk/src/rendezvous/channels",
"!matrix-js-sdk/src/indexeddb-worker",
"!matrix-js-sdk/src/pushprocessor",
"!matrix-js-sdk/src/extensible_events_v1",
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/static_analysis.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}

Expand Down
4 changes: 4 additions & 0 deletions res/css/structures/_UserMenu.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion src/components/structures/MatrixChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -764,7 +764,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
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,
Expand Down
60 changes: 57 additions & 3 deletions src/components/structures/UserMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -52,6 +52,8 @@ 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";
import { Features } from "../../settings/Settings";

interface IProps {
isPanelCollapsed: boolean;
Expand All @@ -66,6 +68,8 @@ interface IState {
isHighContrast: boolean;
selectedSpace?: Room | null;
showLiveAvatarAddon: boolean;
showQrLogin: boolean;
supportsQrLogin: boolean;
}

const toRightOf = (rect: PartialDOMRect): MenuProps => {
Expand Down Expand Up @@ -103,6 +107,8 @@ export default class UserMenu extends React.Component<IProps, IState> {
isHighContrast: this.isUserOnHighContrastTheme(),
selectedSpace: SpaceStore.instance.activeSpaceRoom,
showLiveAvatarAddon: this.context.voiceBroadcastRecordingsStore.hasCurrent(),
showQrLogin: false,
supportsQrLogin: false,
};

OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate);
Expand All @@ -126,6 +132,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
);
this.dispatcherRef = defaultDispatcher.register(this.onAction);
this.themeWatcherRef = SettingsStore.watchSetting("theme", null, this.onThemeChanged);
this.checkQrLoginSupport();
}

public componentWillUnmount(): void {
Expand All @@ -140,6 +147,29 @@ export default class UserMenu extends React.Component<IProps, IState> {
);
}

private checkQrLoginSupport = async (): Promise<void> => {
if (!this.context.client || !SettingsStore.getValue(Features.OidcNativeFlow)) return;

const { issuer } = await this.context.client.getAuthIssuer().catch(() => ({ issuer: undefined }));
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(),
]);

const supportsQrLogin = shouldShowQr(
this.context.client,
!!isCrossSigningReady,
oidcClientConfig,
versions,
wellKnown,
);
this.setState({ supportsQrLogin, showQrLogin: true });
}
};

private isUserOnDarkTheme(): boolean {
if (SettingsStore.getValue("use_system_theme")) {
return window.matchMedia("(prefers-color-scheme: dark)").matches;
Expand Down Expand Up @@ -237,11 +267,11 @@ export default class UserMenu extends React.Component<IProps, IState> {
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<string, any>): 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
};
Expand Down Expand Up @@ -363,9 +393,33 @@ export default class UserMenu extends React.Component<IProps, IState> {
);
}

let linkNewDeviceButton: JSX.Element | undefined;
if (this.state.showQrLogin) {
const extraProps: Omit<
React.ComponentProps<typeof IconizedContextMenuOption>,
"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 = (
<IconizedContextMenuOption
{...extraProps}
iconClassName="mx_UserMenu_iconQr"
label={_t("user_menu|link_new_device")}
onClick={(e) => this.onSettingsOpen(e, UserTab.SessionManager, { showMsc4108QrCode: true })}
/>
);
}

let primaryOptionList = (
<IconizedContextMenuOptionList>
{homeButton}
{linkNewDeviceButton}
<IconizedContextMenuOption
iconClassName="mx_UserMenu_iconBell"
label={_t("notifications|enable_prompt_toast_title")}
Expand Down
10 changes: 7 additions & 3 deletions src/components/views/auth/LoginWithQR-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,21 @@ export enum Mode {
export enum Phase {
Loading,
ShowingQR,
Connecting,
Connected,
// The following are specific to MSC4108
OutOfBandConfirmation,
WaitingForDevice,
Verifying,
Error,
/**
* @deprecated the MSC3906 implementation is deprecated in favour of MSC4108.
*/
LegacyConnected,
}

export enum Click {
Cancel,
Decline,
Approve,
TryAgain,
Back,
ShowQr,
}
Loading

0 comments on commit 1677ed1

Please sign in to comment.