mirror of
https://github.com/vector-im/element-web.git
synced 2026-05-05 12:16:53 +02:00
Prototype implementation of MSC4108 version 2025
This commit is contained in:
parent
f5e56cc8d5
commit
413e2e5c82
@ -297,26 +297,15 @@ async function attemptOidcNativeLogin(queryParams: QueryDict): Promise<boolean>
|
||||
const { accessToken, refreshToken, homeserverUrl, identityServerUrl, idToken, clientId, issuer } =
|
||||
await completeOidcLogin(queryParams);
|
||||
|
||||
const {
|
||||
user_id: userId,
|
||||
device_id: deviceId,
|
||||
is_guest: isGuest,
|
||||
} = await getUserIdFromAccessToken(accessToken, homeserverUrl, identityServerUrl);
|
||||
|
||||
const credentials = {
|
||||
await configureFromCompletedOAuthLogin({
|
||||
accessToken,
|
||||
refreshToken,
|
||||
homeserverUrl,
|
||||
identityServerUrl,
|
||||
deviceId,
|
||||
userId,
|
||||
isGuest,
|
||||
};
|
||||
|
||||
logger.debug("Logged in via OIDC native flow");
|
||||
await onSuccessfulDelegatedAuthLogin(credentials);
|
||||
// this needs to happen after success handler which clears storages
|
||||
persistOidcAuthenticatedSettings(clientId, issuer, idToken);
|
||||
clientId,
|
||||
issuer,
|
||||
idToken,
|
||||
})
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error("Failed to login via OIDC", error);
|
||||
@ -326,6 +315,47 @@ async function attemptOidcNativeLogin(queryParams: QueryDict): Promise<boolean>
|
||||
}
|
||||
}
|
||||
|
||||
export async function configureFromCompletedOAuthLogin({
|
||||
accessToken,
|
||||
refreshToken,
|
||||
homeserverUrl,
|
||||
identityServerUrl,
|
||||
clientId,
|
||||
issuer,
|
||||
idToken,
|
||||
}: {
|
||||
accessToken: string;
|
||||
refreshToken?: string;
|
||||
homeserverUrl: string;
|
||||
identityServerUrl?: string;
|
||||
clientId: string;
|
||||
issuer: string;
|
||||
idToken: string;
|
||||
}): Promise<IMatrixClientCreds> {
|
||||
const {
|
||||
user_id: userId,
|
||||
device_id: deviceId,
|
||||
is_guest: guest,
|
||||
} = await getUserIdFromAccessToken(accessToken, homeserverUrl, identityServerUrl);
|
||||
|
||||
const credentials = {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
homeserverUrl,
|
||||
identityServerUrl,
|
||||
deviceId,
|
||||
userId,
|
||||
guest,
|
||||
} satisfies IMatrixClientCreds;
|
||||
|
||||
logger.debug("Logged in via OIDC native flow");
|
||||
await onSuccessfulDelegatedAuthLogin(credentials);
|
||||
// this needs to happen after success handler which clears storages
|
||||
persistOidcAuthenticatedSettings(clientId, issuer, idToken);
|
||||
|
||||
return credentials;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets information about the owner of a given access token.
|
||||
* @param accessToken
|
||||
|
||||
45
src/Login.ts
45
src/Login.ts
@ -15,6 +15,8 @@ import {
|
||||
type ILoginFlow,
|
||||
type LoginRequest,
|
||||
type OidcClientConfig,
|
||||
DEVICE_AUTHORIZATION_GRANT_TYPE,
|
||||
type IServerVersions,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
@ -30,7 +32,12 @@ import { isUserRegistrationSupported } from "./utils/oidc/isUserRegistrationSupp
|
||||
* LoginFlow type use the client API /login endpoint
|
||||
* OidcNativeFlow is specific to this client
|
||||
*/
|
||||
export type ClientLoginFlow = LoginFlow | OidcNativeFlow;
|
||||
export type ClientLoginFlow = LoginFlow | OidcNativeFlow | LoginWithQrFlow;
|
||||
|
||||
export interface LoginWithQrFlow {
|
||||
type: "loginWithQrFlow";
|
||||
clientId: string;
|
||||
}
|
||||
|
||||
interface ILoginOptions {
|
||||
defaultDeviceDisplayName?: string;
|
||||
@ -115,7 +122,17 @@ export default class Login {
|
||||
SdkConfig.get().oidc_static_clients,
|
||||
isRegistration,
|
||||
);
|
||||
return [oidcFlow];
|
||||
|
||||
let possibleQrFlow: LoginWithQrFlow | undefined;
|
||||
try {
|
||||
const versions = await this.createTemporaryClient().getVersions();
|
||||
// we reuse the clientId from the oidcFlow for QR login
|
||||
// it might be that we later find that the homeserver is different and we initialise a new client
|
||||
possibleQrFlow = tryInitLoginWithQRFlow(this.delegatedAuthentication, versions, oidcFlow.clientId);
|
||||
} catch (e) {
|
||||
logger.warn("Could not fetch server versions for login with QR support, assuming unsupported", e);
|
||||
}
|
||||
return possibleQrFlow ? [possibleQrFlow, oidcFlow] : [oidcFlow];
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
@ -238,6 +255,30 @@ const tryInitOidcNativeFlow = async (
|
||||
return flow;
|
||||
};
|
||||
|
||||
const tryInitLoginWithQRFlow = (
|
||||
oidcClientConfig: OidcClientConfig,
|
||||
versions: IServerVersions,
|
||||
clientId: string,
|
||||
): LoginWithQrFlow | undefined => {
|
||||
const msc4108Supported = !!versions?.unstable_features?.["org.matrix.msc4108"];
|
||||
|
||||
const deviceAuthorizationGrantSupported = oidcClientConfig?.grant_types_supported.includes(
|
||||
DEVICE_AUTHORIZATION_GRANT_TYPE,
|
||||
);
|
||||
|
||||
const qrFeatureEnabled = !!deviceAuthorizationGrantSupported && msc4108Supported;
|
||||
|
||||
console.log("qrFeatureEnabled", qrFeatureEnabled);
|
||||
if (!qrFeatureEnabled) return undefined;
|
||||
|
||||
const flow = {
|
||||
type: "loginWithQrFlow",
|
||||
clientId
|
||||
} satisfies LoginWithQrFlow;
|
||||
|
||||
return flow;
|
||||
};
|
||||
|
||||
/**
|
||||
* Send a login request to the given server, and format the response
|
||||
* as a MatrixClientCreds
|
||||
|
||||
@ -2044,9 +2044,11 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
* Note: SSO users (and any others using token login) currently do not pass through
|
||||
* this, as they instead jump straight into the app after `attemptTokenLogin`.
|
||||
*/
|
||||
private onUserCompletedLoginFlow = async (credentials: IMatrixClientCreds): Promise<void> => {
|
||||
private onUserCompletedLoginFlow = async (credentials: IMatrixClientCreds, alreadySignedIn = false): Promise<void> => {
|
||||
// Create and start the client
|
||||
await Lifecycle.setLoggedIn(credentials);
|
||||
if (!alreadySignedIn) {
|
||||
await Lifecycle.setLoggedIn(credentials);
|
||||
}
|
||||
await this.postLoginSetup();
|
||||
|
||||
PerformanceMonitor.instance.stop(PerformanceEntryNames.LOGIN);
|
||||
|
||||
@ -9,10 +9,12 @@ Please see LICENSE files in the repository root for full details.
|
||||
import React, { type JSX, type ReactNode } from "react";
|
||||
import classNames from "classnames";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { type SSOFlow, SSOAction } from "matrix-js-sdk/src/matrix";
|
||||
import { type SSOFlow, SSOAction, type MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { QrCodeIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
import { secureRandomString } from "matrix-js-sdk/src/randomstring";
|
||||
|
||||
import { _t, UserFriendlyError } from "../../../languageHandler";
|
||||
import Login, { type ClientLoginFlow, type OidcNativeFlow } from "../../../Login";
|
||||
import Login, { type LoginWithQrFlow, type ClientLoginFlow, type OidcNativeFlow } from "../../../Login";
|
||||
import { messageForConnectionError, messageForLoginError } from "../../../utils/ErrorUtils";
|
||||
import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils";
|
||||
import AuthPage from "../../views/auth/AuthPage";
|
||||
@ -31,6 +33,9 @@ import AccessibleButton, { type ButtonEvent } from "../../views/elements/Accessi
|
||||
import { type ValidatedServerConfig } from "../../../utils/ValidatedServerConfig";
|
||||
import { filterBoolean } from "../../../utils/arrays";
|
||||
import { startOidcLogin } from "../../../utils/oidc/authorize";
|
||||
import LoginWithQR from "../../views/auth/LoginWithQR";
|
||||
import { Mode } from "../../views/auth/LoginWithQR-types";
|
||||
import createMatrixClient from "../../../utils/createMatrixClient";
|
||||
|
||||
interface IProps {
|
||||
serverConfig: ValidatedServerConfig;
|
||||
@ -47,7 +52,8 @@ interface IProps {
|
||||
|
||||
// Called when the user has logged in. Params:
|
||||
// - The object returned by the login API
|
||||
onLoggedIn(data: IMatrixClientCreds): void;
|
||||
// - alreadySignedIn: true if the user was already signed in (e.g. QR login) and only the post login setup is needed
|
||||
onLoggedIn(data: IMatrixClientCreds, alreadySignedIn?: boolean): void;
|
||||
|
||||
// login shouldn't know or care how registration, password recovery, etc is done.
|
||||
onRegisterClick(): void;
|
||||
@ -77,6 +83,9 @@ interface IState {
|
||||
serverIsAlive: boolean;
|
||||
serverErrorIsFatal: boolean;
|
||||
serverDeadError?: ReactNode;
|
||||
|
||||
loginWithQrInProgress: boolean;
|
||||
loginWithQrClient?: MatrixClient;
|
||||
}
|
||||
|
||||
type OnPasswordLogin = {
|
||||
@ -109,6 +118,8 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
|
||||
serverIsAlive: true,
|
||||
serverErrorIsFatal: false,
|
||||
serverDeadError: "",
|
||||
|
||||
loginWithQrInProgress: false,
|
||||
};
|
||||
|
||||
// map from login step type to a function which will render a control
|
||||
@ -122,6 +133,7 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
"m.login.sso": () => this.renderSsoStep("sso"),
|
||||
"oidcNativeFlow": () => this.renderOidcNativeStep(),
|
||||
"loginWithQrFlow": () => this.renderLoginWithQRStep(),
|
||||
};
|
||||
}
|
||||
|
||||
@ -403,7 +415,7 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
|
||||
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 = ["loginWithQrFlow", "oidcNativeFlow", "m.login.password", "m.login.sso"];
|
||||
|
||||
const flows = filterBoolean(order.map((type) => this.state.flows?.find((flow) => flow.type === type)));
|
||||
return (
|
||||
@ -456,6 +468,39 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
|
||||
);
|
||||
};
|
||||
|
||||
private startLoginWithQR = (): void => {
|
||||
if (this.state.loginWithQrInProgress) return;
|
||||
// pick our device ID
|
||||
const deviceId = secureRandomString(10);
|
||||
const loginWithQrClient = createMatrixClient({ baseUrl: this.loginLogic.getHomeserverUrl(), idBaseUrl: this.loginLogic.getIdentityServerUrl(), deviceId });
|
||||
this.setState({ loginWithQrInProgress: true, loginWithQrClient });
|
||||
};
|
||||
|
||||
private renderLoginWithQRStep = (): JSX.Element => {
|
||||
return (
|
||||
<>
|
||||
<p className="mx_Login_withQR_or">or</p>
|
||||
<AccessibleButton
|
||||
className="mx_Login_withQR"
|
||||
kind="primary_outline"
|
||||
onClick={this.startLoginWithQR}
|
||||
>
|
||||
<QrCodeIcon />
|
||||
{_t("Sign in with QR code")}
|
||||
</AccessibleButton>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
private onLoginWithQRFinished = (success: boolean, credentials?: IMatrixClientCreds): void => {
|
||||
if (credentials) {
|
||||
this.props.onLoggedIn(credentials, true);
|
||||
} else if (!success) {
|
||||
this.state.loginWithQrClient?.stopClient();
|
||||
this.setState({ loginWithQrInProgress: false, loginWithQrClient: undefined });
|
||||
}
|
||||
};
|
||||
|
||||
private renderSsoStep = (loginType: "cas" | "sso"): JSX.Element => {
|
||||
const flow = this.state.flows?.find((flow) => flow.type === "m.login." + loginType) as SSOFlow;
|
||||
|
||||
@ -472,6 +517,10 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
|
||||
);
|
||||
};
|
||||
|
||||
private get qrClientId(): string {
|
||||
return (this.state.flows?.find((flow) => flow.type === "loginWithQrFlow") as LoginWithQrFlow).clientId ?? '';
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
const loader =
|
||||
this.isBusy() && !this.state.busyLoggingIn ? (
|
||||
@ -532,19 +581,22 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
|
||||
<AuthPage>
|
||||
<AuthHeader disableLanguageSelector={this.props.isSyncing || this.state.busyLoggingIn} />
|
||||
<AuthBody>
|
||||
<h1>
|
||||
{_t("action|sign_in")}
|
||||
{loader}
|
||||
</h1>
|
||||
{errorTextSection}
|
||||
{serverDeadSection}
|
||||
<ServerPicker
|
||||
serverConfig={this.props.serverConfig}
|
||||
onServerConfigChange={this.props.onServerConfigChange}
|
||||
disabled={this.isBusy()}
|
||||
/>
|
||||
{this.renderLoginComponentForFlows()}
|
||||
{footer}
|
||||
{this.state.loginWithQrInProgress ? (<><LoginWithQR onFinished={this.onLoginWithQRFinished} mode={Mode.Show} client={this.state.loginWithQrClient!} clientId={this.qrClientId} /></>) : (
|
||||
<>
|
||||
<h1>
|
||||
{_t("action|sign_in")}
|
||||
{loader}
|
||||
</h1>
|
||||
{errorTextSection}
|
||||
{serverDeadSection}
|
||||
<ServerPicker
|
||||
serverConfig={this.props.serverConfig}
|
||||
onServerConfigChange={this.props.onServerConfigChange}
|
||||
disabled={this.isBusy()}
|
||||
/>
|
||||
{this.renderLoginComponentForFlows()}
|
||||
{footer}
|
||||
</>)}
|
||||
</AuthBody>
|
||||
</AuthPage>
|
||||
);
|
||||
|
||||
@ -18,16 +18,20 @@ import {
|
||||
RendezvousIntent,
|
||||
} from "matrix-js-sdk/src/rendezvous";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { QrCodeIntent } from "@matrix-org/matrix-sdk-crypto-wasm";
|
||||
|
||||
import { Click, Mode, Phase } from "./LoginWithQR-types";
|
||||
import LoginWithQRFlow from "./LoginWithQRFlow";
|
||||
import { configureFromCompletedOAuthLogin, restoreSessionFromStorage } from "../../../Lifecycle";
|
||||
import { MatrixClientPeg, type IMatrixClientCreds } from "../../../MatrixClientPeg";
|
||||
|
||||
interface IProps {
|
||||
client: MatrixClient;
|
||||
mode: Mode;
|
||||
onFinished(...args: any): void;
|
||||
}
|
||||
client: MatrixClient;
|
||||
clientId: string;
|
||||
onFinished(success: boolean, credentials?: IMatrixClientCreds): void;
|
||||
};
|
||||
|
||||
interface IState {
|
||||
phase: Phase;
|
||||
@ -37,6 +41,8 @@ interface IState {
|
||||
userCode?: string;
|
||||
checkCode?: string;
|
||||
failureReason?: FailureReason;
|
||||
homeserverBaseUrl?: string;
|
||||
newClient?: MatrixClient;
|
||||
}
|
||||
|
||||
export enum LoginWithQRFailureReason {
|
||||
@ -65,7 +71,7 @@ export default class LoginWithQR extends React.Component<IProps, IState> {
|
||||
}
|
||||
|
||||
private get ourIntent(): RendezvousIntent {
|
||||
return RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE;
|
||||
return this.props.client.getUserId() ? RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE : RendezvousIntent.LOGIN_ON_NEW_DEVICE;
|
||||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
@ -99,20 +105,25 @@ export default class LoginWithQR extends React.Component<IProps, IState> {
|
||||
}
|
||||
}
|
||||
|
||||
private onFinished(success: boolean): void {
|
||||
private onFinished(success: boolean, credentials?: IMatrixClientCreds): void {
|
||||
this.finished = true;
|
||||
this.props.onFinished(success);
|
||||
this.props.onFinished(success, credentials);
|
||||
}
|
||||
|
||||
private generateAndShowCode = async (): Promise<void> => {
|
||||
let rendezvous: MSC4108SignInWithQR;
|
||||
// init rust crypto as needed for the secure channel
|
||||
const RustSdkCryptoJs = await import("@matrix-org/matrix-sdk-crypto-wasm");
|
||||
await RustSdkCryptoJs.initAsync();
|
||||
|
||||
let rendezvous: MSC4108SignInWithQR | undefined;
|
||||
|
||||
try {
|
||||
const transport = new MSC4108RendezvousSession({
|
||||
onFailure: this.onFailure,
|
||||
client: this.props.client,
|
||||
});
|
||||
await transport.send("");
|
||||
const channel = new MSC4108SecureChannel(transport, undefined, this.onFailure);
|
||||
const channel = new MSC4108SecureChannel(transport, this.ourIntent === RendezvousIntent.LOGIN_ON_NEW_DEVICE ? QrCodeIntent.Login : QrCodeIntent.Reciprocate, undefined, this.onFailure);
|
||||
rendezvous = new MSC4108SignInWithQR(channel, false, this.props.client, this.onFailure);
|
||||
|
||||
await rendezvous.generateCode();
|
||||
@ -136,6 +147,12 @@ export default class LoginWithQR extends React.Component<IProps, IState> {
|
||||
phase: Phase.OutOfBandConfirmation,
|
||||
verificationUri,
|
||||
});
|
||||
} else {
|
||||
const { baseUrl } = await rendezvous.negotiateProtocols();
|
||||
this.setState({
|
||||
phase: Phase.OutOfBandConfirmation,
|
||||
homeserverBaseUrl: baseUrl,
|
||||
});
|
||||
}
|
||||
|
||||
// we ask the user to confirm that the channel is secure
|
||||
@ -175,8 +192,70 @@ export default class LoginWithQR extends React.Component<IProps, IState> {
|
||||
// done
|
||||
this.onFinished(true);
|
||||
} else {
|
||||
this.setState({ phase: Phase.Error, failureReason: ClientRendezvousFailureReason.Unknown });
|
||||
throw new Error("New device flows around OIDC are not yet implemented");
|
||||
|
||||
if (!this.state.homeserverBaseUrl) {
|
||||
this.setState({ phase: Phase.Error, failureReason: ClientRendezvousFailureReason.Unknown });
|
||||
throw new Error("Homeserver base URL not found in state");
|
||||
}
|
||||
|
||||
if (new URL(this.props.client.baseUrl).toString() !== new URL(this.state.homeserverBaseUrl).toString()) {
|
||||
// would need to switch homeservers
|
||||
this.setState({ phase: Phase.Error, failureReason: ClientRendezvousFailureReason.Unknown });
|
||||
logger.info(`Changing homeservers during new device login not yet supported: ${this.props.client.baseUrl} -> ${this.state.homeserverBaseUrl}`);
|
||||
throw new Error("Changing homeservers during new device login not yet supported");
|
||||
}
|
||||
|
||||
const metadata = await this.props.client.getAuthMetadata();
|
||||
const deviceId = this.props.client.getDeviceId()!;
|
||||
const { userCode } = await this.state.rendezvous.deviceAuthorizationGrant({
|
||||
metadata,
|
||||
clientId: this.props.clientId,
|
||||
deviceId,
|
||||
});
|
||||
this.setState({ phase: Phase.WaitingForDevice, userCode });
|
||||
|
||||
const datr = await this.state.rendezvous.completeLoginOnNewDevice({
|
||||
clientId: this.props.clientId,
|
||||
});
|
||||
|
||||
if (datr) {
|
||||
// PROTOTYPE: this is probably not the right way to do this
|
||||
|
||||
// store and use the new credentials
|
||||
const credentials = await configureFromCompletedOAuthLogin({
|
||||
accessToken: datr.access_token,
|
||||
refreshToken: datr.refresh_token,
|
||||
homeserverUrl: this.state.homeserverBaseUrl,
|
||||
clientId: this.props.clientId,
|
||||
idToken: datr.id_token ?? '', // is this actually optional?
|
||||
issuer: metadata!.issuer,
|
||||
identityServerUrl: undefined, // PROTOTYPE: we should have stored this from before
|
||||
});
|
||||
|
||||
const { secrets } = await this.state.rendezvous.shareSecrets();
|
||||
|
||||
await restoreSessionFromStorage();
|
||||
|
||||
if (secrets) {
|
||||
const crypto = MatrixClientPeg.safeGet().getCrypto();
|
||||
if (crypto) {
|
||||
await crypto.importSecretsBundle?.(secrets);
|
||||
// it should be sufficient to just upload the device keys with the signature
|
||||
// but this seems to do the job for now
|
||||
await crypto.crossSignDevice(deviceId);
|
||||
} else {
|
||||
logger.warn("Crypto not initialised");
|
||||
}
|
||||
} else {
|
||||
logger.warn("No secrets received from QR login");
|
||||
}
|
||||
|
||||
// PROTOTYPE: fudge to try and allow the self verification to complete before we change screen
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
|
||||
// done
|
||||
this.onFinished(true, credentials);
|
||||
}
|
||||
}
|
||||
} catch (e: RendezvousError | unknown) {
|
||||
logger.error("Error whilst approving sign in", e);
|
||||
|
||||
@ -132,11 +132,6 @@ export default class LoginWithQRFlow extends React.Component<Props> {
|
||||
message = _t("auth|qr_code_login|error_rate_limited");
|
||||
break;
|
||||
|
||||
case ClientRendezvousFailureReason.ETagMissing:
|
||||
title = _t("error|something_went_wrong");
|
||||
message = _t("auth|qr_code_login|error_etag_missing");
|
||||
break;
|
||||
|
||||
case ClientRendezvousFailureReason.HomeserverLacksSupport:
|
||||
success = null;
|
||||
Icon = QrCodeIcon;
|
||||
|
||||
@ -11,7 +11,7 @@ import {
|
||||
type IServerVersions,
|
||||
type OidcClientConfig,
|
||||
type MatrixClient,
|
||||
DEVICE_CODE_SCOPE,
|
||||
DEVICE_AUTHORIZATION_GRANT_TYPE,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import QrCodeIcon from "@vector-im/compound-design-tokens/assets/web/icons/qr-code";
|
||||
import { Text } from "@vector-im/compound-web";
|
||||
@ -28,7 +28,7 @@ interface IProps {
|
||||
isCrossSigningReady?: boolean;
|
||||
}
|
||||
|
||||
export function shouldShowQr(
|
||||
export function shouldShowQrForReciprocate(
|
||||
cli: MatrixClient,
|
||||
isCrossSigningReady: boolean,
|
||||
oidcClientConfig?: OidcClientConfig,
|
||||
@ -36,7 +36,7 @@ export function shouldShowQr(
|
||||
): boolean {
|
||||
const msc4108Supported = !!versions?.unstable_features?.["org.matrix.msc4108"];
|
||||
|
||||
const deviceAuthorizationGrantSupported = oidcClientConfig?.grant_types_supported.includes(DEVICE_CODE_SCOPE);
|
||||
const deviceAuthorizationGrantSupported = oidcClientConfig?.grant_types_supported.includes(DEVICE_AUTHORIZATION_GRANT_TYPE);
|
||||
|
||||
return (
|
||||
!!deviceAuthorizationGrantSupported &&
|
||||
@ -48,7 +48,7 @@ export function shouldShowQr(
|
||||
|
||||
const LoginWithQRSection: React.FC<IProps> = ({ onShowQr, versions, oidcClientConfig, isCrossSigningReady }) => {
|
||||
const cli = useMatrixClientContext();
|
||||
const offerShowQr = shouldShowQr(cli, !!isCrossSigningReady, oidcClientConfig, versions);
|
||||
const offerShowQr = shouldShowQrForReciprocate(cli, !!isCrossSigningReady, oidcClientConfig, versions);
|
||||
|
||||
return (
|
||||
<SettingsSubsection heading={_t("settings|sessions|sign_in_with_qr")}>
|
||||
|
||||
@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import React from "react";
|
||||
import { render, screen, waitFor } from "jest-matrix-react";
|
||||
import { DEVICE_CODE_SCOPE, type MatrixClient, type Room } from "matrix-js-sdk/src/matrix";
|
||||
import { DEVICE_AUTHORIZATION_GRANT_TYPE, type MatrixClient, type Room } from "matrix-js-sdk/src/matrix";
|
||||
import { type CryptoApi } from "matrix-js-sdk/src/crypto-api";
|
||||
import { mocked } from "jest-mock";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
@ -113,7 +113,7 @@ describe("<UserMenu>", () => {
|
||||
it("should render 'Link new device' button in OIDC native mode", async () => {
|
||||
sdkContext.client = stubClient();
|
||||
const openIdMetadata = mockOpenIdConfiguration("https://issuer/");
|
||||
openIdMetadata.grant_types_supported.push(DEVICE_CODE_SCOPE);
|
||||
openIdMetadata.grant_types_supported.push(DEVICE_AUTHORIZATION_GRANT_TYPE);
|
||||
fetchMock.get("https://issuer/.well-known/openid-configuration", openIdMetadata);
|
||||
fetchMock.get("https://issuer/jwks", {
|
||||
status: 200,
|
||||
|
||||
@ -36,7 +36,7 @@ function makeClient() {
|
||||
getUser: jest.fn(),
|
||||
isGuest: jest.fn().mockReturnValue(false),
|
||||
isUserIgnored: jest.fn(),
|
||||
getUserId: jest.fn(),
|
||||
getUserId: jest.fn().mockReturnValue("@user:server"),
|
||||
on: jest.fn(),
|
||||
isSynapseAdministrator: jest.fn().mockResolvedValue(false),
|
||||
isRoomEncrypted: jest.fn().mockReturnValue(false),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user