MSC4108 support OIDC QR code login (#12370)

Co-authored-by: Hugh Nimmo-Smith <hughns@matrix.org>
This commit is contained in:
Michael Telatynski 2024-06-06 09:57:28 +01:00 committed by GitHub
parent ca7760789b
commit 1677ed1be0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 1558 additions and 733 deletions

View file

@ -21,18 +21,26 @@ import {
GET_LOGIN_TOKEN_CAPABILITY,
Capabilities,
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";
import { _t } from "../../../../languageHandler";
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;
versions?: IServerVersions;
capabilities?: Capabilities;
wellKnown?: IClientWellKnown;
oidcClientConfig?: OidcClientConfig;
isCrossSigningReady?: boolean;
}
function shouldShowQrLegacy(
@ -50,8 +58,40 @@ function shouldShowQrLegacy(
return getLoginTokenSupported && msc3886Supported;
}
const LoginWithQRSection: React.FC<IProps> = ({ onShowQr, versions, capabilities, wellKnown }) => {
const offerShowQr = shouldShowQrLegacy(versions, wellKnown, capabilities);
export function shouldShowQr(
cli: MatrixClient,
isCrossSigningReady: boolean,
oidcClientConfig?: OidcClientConfig,
versions?: IServerVersions,
wellKnown?: IClientWellKnown,
): boolean {
const msc4108Supported =
!!versions?.unstable_features?.["org.matrix.msc4108"] || !!wellKnown?.["io.element.rendezvous"]?.server;
const deviceAuthorizationGrantSupported =
oidcClientConfig?.metadata?.grant_types_supported.includes(DEVICE_CODE_SCOPE);
return (
deviceAuthorizationGrantSupported &&
msc4108Supported &&
SettingsStore.getValue(Features.OidcNativeFlow) &&
!!cli.getCrypto()?.exportSecretsBundle &&
isCrossSigningReady
);
}
const LoginWithQRSection: React.FC<IProps> = ({
onShowQr,
versions,
capabilities,
wellKnown,
oidcClientConfig,
isCrossSigningReady,
}) => {
const cli = useMatrixClientContext();
const offerShowQr = oidcClientConfig
? shouldShowQr(cli, !!isCrossSigningReady, oidcClientConfig, versions, wellKnown)
: shouldShowQrLegacy(versions, wellKnown, capabilities);
// don't show anything if no method is available
if (!offerShowQr) {

View file

@ -14,8 +14,8 @@ 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 { MatrixClient } from "matrix-js-sdk/src/matrix";
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";
import { _t } from "../../../../../languageHandler";
@ -32,7 +32,6 @@ import { ExtendedDevice } from "../../devices/types";
import { deleteDevicesWithInteractiveAuth } from "../../devices/deleteDevices";
import SettingsTab from "../SettingsTab";
import LoginWithQRSection from "../../devices/LoginWithQRSection";
import LoginWithQR from "../../../auth/LoginWithQR";
import { Mode } from "../../../auth/LoginWithQR-types";
import { useAsyncMemo } from "../../../../../hooks/useAsyncMemo";
import QuestionDialog from "../../../dialogs/QuestionDialog";
@ -41,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<boolean> => {
const { finished } = Modal.createDialog(QuestionDialog, {
@ -148,7 +151,9 @@ const useSignOut = (
};
};
const SessionManagerTab: React.FC = () => {
const SessionManagerTab: React.FC<{
showMsc4108QrCode?: boolean;
}> = ({ showMsc4108QrCode }) => {
const {
devices,
dehydratedDeviceId,
@ -186,6 +191,20 @@ const SessionManagerTab: React.FC = () => {
const clientVersions = useAsyncMemo(() => matrixClient.getVersions(), [matrixClient]);
const capabilities = useAsyncMemo(async () => matrixClient?.getCapabilities(), [matrixClient]);
const wellKnown = useMemo(() => matrixClient?.getClientWellKnown(), [matrixClient]);
const oidcClientConfig = useAsyncMemo(async () => {
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(
async () => matrixClient.getCrypto()?.isCrossSigningReady() ?? false,
[matrixClient],
);
const onDeviceExpandToggle = (deviceId: ExtendedDevice["device_id"]): void => {
if (expandedDeviceIds.includes(deviceId)) {
@ -268,7 +287,7 @@ const SessionManagerTab: React.FC = () => {
}
: undefined;
const [signInWithQrMode, setSignInWithQrMode] = useState<Mode | null>();
const [signInWithQrMode, setSignInWithQrMode] = useState<Mode | null>(showMsc4108QrCode ? Mode.Show : null);
const onQrFinish = useCallback(() => {
setSignInWithQrMode(null);
@ -279,7 +298,16 @@ const SessionManagerTab: React.FC = () => {
}, [setSignInWithQrMode]);
if (signInWithQrMode) {
return <LoginWithQR mode={signInWithQrMode} onFinished={onQrFinish} client={matrixClient} />;
return (
<Suspense fallback={<Spinner />}>
<LoginWithQR
mode={signInWithQrMode}
onFinished={onQrFinish}
client={matrixClient}
legacy={!oidcClientConfig && !showMsc4108QrCode}
/>
</Suspense>
);
}
return (
@ -290,6 +318,8 @@ const SessionManagerTab: React.FC = () => {
versions={clientVersions}
capabilities={capabilities}
wellKnown={wellKnown}
oidcClientConfig={oidcClientConfig}
isCrossSigningReady={isCrossSigningReady}
/>
<SecurityRecommendations
devices={devices}