OIDC: use delegated auth account URL from OidcClientStore
(#11723)
* test persistCredentials without a pickle key * test setLoggedIn with pickle key * lint * type error * extract token persisting code into function, persist refresh token * store has_refresh_token too * pass refreshToken from oidcAuthGrant into credentials * rest restore session with pickle key * retreive stored refresh token and add to credentials * extract token decryption into function * remove TODO * very messy poc * utils to persist clientId and issuer after oidc authentication * add dep oidc-client-ts * persist issuer and clientId after successful oidc auth * add OidcClientStore * comments and tidy * expose getters for stored refresh and access tokens in Lifecycle * revoke tokens with oidc provider * test logout action in MatrixChat * comments * prettier * test OidcClientStore.revokeTokens * put pickle key destruction back * comment pedantry * working refresh without persistence * extract token persistence functions to utils * add sugar * implement TokenRefresher class with persistence * tidying * persist idTokenClaims * persist idTokenClaims * tests * remove unused cde * create token refresher during doSetLoggedIn * tidying * also tidying * OidcClientStore.initClient use stored issuer when client well known unavailable * test Lifecycle.logout * update Lifecycle test replaceUsingCreds calls * fix test * add sdkContext to UserSettingsDialog * use sdkContext and oidcClientStore in session manager * use sdkContext and OidcClientStore in generalusersettingstab * tidy * test tokenrefresher creation in login flow * test token refresher * Update src/utils/oidc/TokenRefresher.ts Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> * use literal value for m.authentication Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> * improve comments * fix test mock, comment * typo * add sdkContext to SoftLogout, pass oidcClientStore to logout * fullstops * comments * fussy comment formatting --------- Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
This commit is contained in:
parent
84ca519b3f
commit
d9d52fba8c
10 changed files with 96 additions and 179 deletions
|
@ -765,7 +765,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
const tabPayload = payload as OpenToTabPayload;
|
const tabPayload = payload as OpenToTabPayload;
|
||||||
Modal.createDialog(
|
Modal.createDialog(
|
||||||
UserSettingsDialog,
|
UserSettingsDialog,
|
||||||
{ initialTabId: tabPayload.initialTabId as UserTab },
|
{ initialTabId: tabPayload.initialTabId as UserTab, sdkContext: this.stores },
|
||||||
/*className=*/ undefined,
|
/*className=*/ undefined,
|
||||||
/*isPriority=*/ false,
|
/*isPriority=*/ false,
|
||||||
/*isStatic=*/ true,
|
/*isStatic=*/ true,
|
||||||
|
|
|
@ -36,9 +36,11 @@ import KeyboardUserSettingsTab from "../settings/tabs/user/KeyboardUserSettingsT
|
||||||
import SessionManagerTab from "../settings/tabs/user/SessionManagerTab";
|
import SessionManagerTab from "../settings/tabs/user/SessionManagerTab";
|
||||||
import { UserTab } from "./UserTab";
|
import { UserTab } from "./UserTab";
|
||||||
import { NonEmptyArray } from "../../../@types/common";
|
import { NonEmptyArray } from "../../../@types/common";
|
||||||
|
import { SDKContext, SdkContextClass } from "../../../contexts/SDKContext";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
initialTabId?: UserTab;
|
initialTabId?: UserTab;
|
||||||
|
sdkContext: SdkContextClass;
|
||||||
onFinished(): void;
|
onFinished(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -197,20 +199,25 @@ export default class UserSettingsDialog extends React.Component<IProps, IState>
|
||||||
|
|
||||||
public render(): React.ReactNode {
|
public render(): React.ReactNode {
|
||||||
return (
|
return (
|
||||||
<BaseDialog
|
// XXX: SDKContext is provided within the LoggedInView subtree.
|
||||||
className="mx_UserSettingsDialog"
|
// Modals function outside the MatrixChat React tree, so sdkContext is reprovided here to simulate that.
|
||||||
hasCancel={true}
|
// The longer term solution is to move our ModalManager into the React tree to inherit contexts properly.
|
||||||
onFinished={this.props.onFinished}
|
<SDKContext.Provider value={this.props.sdkContext}>
|
||||||
title={_t("common|settings")}
|
<BaseDialog
|
||||||
>
|
className="mx_UserSettingsDialog"
|
||||||
<div className="mx_SettingsDialog_content">
|
hasCancel={true}
|
||||||
<TabbedView
|
onFinished={this.props.onFinished}
|
||||||
tabs={this.getTabs()}
|
title={_t("common|settings")}
|
||||||
initialTabId={this.props.initialTabId}
|
>
|
||||||
screenName="UserSettings"
|
<div className="mx_SettingsDialog_content">
|
||||||
/>
|
<TabbedView
|
||||||
</div>
|
tabs={this.getTabs()}
|
||||||
</BaseDialog>
|
initialTabId={this.props.initialTabId}
|
||||||
|
screenName="UserSettings"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</BaseDialog>
|
||||||
|
</SDKContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,13 +32,13 @@ import { VerificationRequest } from "matrix-js-sdk/src/crypto-api";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
|
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
|
||||||
|
|
||||||
import MatrixClientContext from "../../../../contexts/MatrixClientContext";
|
|
||||||
import { _t } from "../../../../languageHandler";
|
import { _t } from "../../../../languageHandler";
|
||||||
import { getDeviceClientInformation, pruneClientInformation } from "../../../../utils/device/clientInformation";
|
import { getDeviceClientInformation, pruneClientInformation } from "../../../../utils/device/clientInformation";
|
||||||
import { DevicesDictionary, ExtendedDevice, ExtendedDeviceAppInfo } from "./types";
|
import { DevicesDictionary, ExtendedDevice, ExtendedDeviceAppInfo } from "./types";
|
||||||
import { useEventEmitter } from "../../../../hooks/useEventEmitter";
|
import { useEventEmitter } from "../../../../hooks/useEventEmitter";
|
||||||
import { parseUserAgent } from "../../../../utils/device/parseUserAgent";
|
import { parseUserAgent } from "../../../../utils/device/parseUserAgent";
|
||||||
import { isDeviceVerified } from "../../../../utils/device/isDeviceVerified";
|
import { isDeviceVerified } from "../../../../utils/device/isDeviceVerified";
|
||||||
|
import { SDKContext } from "../../../../contexts/SDKContext";
|
||||||
|
|
||||||
const parseDeviceExtendedInformation = (matrixClient: MatrixClient, device: IMyDevice): ExtendedDeviceAppInfo => {
|
const parseDeviceExtendedInformation = (matrixClient: MatrixClient, device: IMyDevice): ExtendedDeviceAppInfo => {
|
||||||
const { name, version, url } = getDeviceClientInformation(matrixClient, device.device_id);
|
const { name, version, url } = getDeviceClientInformation(matrixClient, device.device_id);
|
||||||
|
@ -90,7 +90,8 @@ export type DevicesState = {
|
||||||
supportsMSC3881?: boolean | undefined;
|
supportsMSC3881?: boolean | undefined;
|
||||||
};
|
};
|
||||||
export const useOwnDevices = (): DevicesState => {
|
export const useOwnDevices = (): DevicesState => {
|
||||||
const matrixClient = useContext(MatrixClientContext);
|
const sdkContext = useContext(SDKContext);
|
||||||
|
const matrixClient = sdkContext.client!;
|
||||||
|
|
||||||
const currentDeviceId = matrixClient.getDeviceId()!;
|
const currentDeviceId = matrixClient.getDeviceId()!;
|
||||||
const userId = matrixClient.getSafeUserId();
|
const userId = matrixClient.getSafeUserId();
|
||||||
|
|
|
@ -56,9 +56,8 @@ import SettingsSubsection, { SettingsSubsectionText } from "../../shared/Setting
|
||||||
import { SettingsSubsectionHeading } from "../../shared/SettingsSubsectionHeading";
|
import { SettingsSubsectionHeading } from "../../shared/SettingsSubsectionHeading";
|
||||||
import Heading from "../../../typography/Heading";
|
import Heading from "../../../typography/Heading";
|
||||||
import InlineSpinner from "../../../elements/InlineSpinner";
|
import InlineSpinner from "../../../elements/InlineSpinner";
|
||||||
import MatrixClientContext from "../../../../../contexts/MatrixClientContext";
|
|
||||||
import { ThirdPartyIdentifier } from "../../../../../AddThreepid";
|
import { ThirdPartyIdentifier } from "../../../../../AddThreepid";
|
||||||
import { getDelegatedAuthAccountUrl } from "../../../../../utils/oidc/getDelegatedAuthAccountUrl";
|
import { SDKContext } from "../../../../../contexts/SDKContext";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
closeSettingsFn: () => void;
|
closeSettingsFn: () => void;
|
||||||
|
@ -94,20 +93,22 @@ interface IState {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class GeneralUserSettingsTab extends React.Component<IProps, IState> {
|
export default class GeneralUserSettingsTab extends React.Component<IProps, IState> {
|
||||||
public static contextType = MatrixClientContext;
|
public static contextType = SDKContext;
|
||||||
public context!: React.ContextType<typeof MatrixClientContext>;
|
public context!: React.ContextType<typeof SDKContext>;
|
||||||
|
|
||||||
private readonly dispatcherRef: string;
|
private readonly dispatcherRef: string;
|
||||||
|
|
||||||
public constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) {
|
public constructor(props: IProps, context: React.ContextType<typeof SDKContext>) {
|
||||||
super(props);
|
super(props);
|
||||||
this.context = context;
|
this.context = context;
|
||||||
|
|
||||||
|
const cli = this.context.client!;
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
language: languageHandler.getCurrentLanguage(),
|
language: languageHandler.getCurrentLanguage(),
|
||||||
spellCheckEnabled: false,
|
spellCheckEnabled: false,
|
||||||
spellCheckLanguages: [],
|
spellCheckLanguages: [],
|
||||||
haveIdServer: Boolean(this.context.getIdentityServerUrl()),
|
haveIdServer: Boolean(cli.getIdentityServerUrl()),
|
||||||
idServerHasUnsignedTerms: false,
|
idServerHasUnsignedTerms: false,
|
||||||
requiredPolicyInfo: {
|
requiredPolicyInfo: {
|
||||||
// This object is passed along to a component for handling
|
// This object is passed along to a component for handling
|
||||||
|
@ -150,7 +151,7 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
|
||||||
|
|
||||||
private onAction = (payload: ActionPayload): void => {
|
private onAction = (payload: ActionPayload): void => {
|
||||||
if (payload.action === "id_server_changed") {
|
if (payload.action === "id_server_changed") {
|
||||||
this.setState({ haveIdServer: Boolean(this.context.getIdentityServerUrl()) });
|
this.setState({ haveIdServer: Boolean(this.context.client!.getIdentityServerUrl()) });
|
||||||
this.getThreepidState();
|
this.getThreepidState();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -164,7 +165,7 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
|
||||||
};
|
};
|
||||||
|
|
||||||
private async getCapabilities(): Promise<void> {
|
private async getCapabilities(): Promise<void> {
|
||||||
const cli = this.context;
|
const cli = this.context.client!;
|
||||||
|
|
||||||
const capabilities = await cli.getCapabilities(); // this is cached
|
const capabilities = await cli.getCapabilities(); // this is cached
|
||||||
const changePasswordCap = capabilities["m.change_password"];
|
const changePasswordCap = capabilities["m.change_password"];
|
||||||
|
@ -174,7 +175,7 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
|
||||||
// the enabled flag value.
|
// the enabled flag value.
|
||||||
const canChangePassword = !changePasswordCap || changePasswordCap["enabled"] !== false;
|
const canChangePassword = !changePasswordCap || changePasswordCap["enabled"] !== false;
|
||||||
|
|
||||||
const externalAccountManagementUrl = getDelegatedAuthAccountUrl(cli.getClientWellKnown());
|
const externalAccountManagementUrl = this.context.oidcClientStore.accountManagementEndpoint;
|
||||||
// https://spec.matrix.org/v1.7/client-server-api/#m3pid_changes-capability
|
// https://spec.matrix.org/v1.7/client-server-api/#m3pid_changes-capability
|
||||||
// We support as far back as v1.1 which doesn't have m.3pid_changes
|
// We support as far back as v1.1 which doesn't have m.3pid_changes
|
||||||
// so the behaviour for when it is missing has to be assume true
|
// so the behaviour for when it is missing has to be assume true
|
||||||
|
@ -184,7 +185,7 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getThreepidState(): Promise<void> {
|
private async getThreepidState(): Promise<void> {
|
||||||
const cli = this.context;
|
const cli = this.context.client!;
|
||||||
|
|
||||||
// Check to see if terms need accepting
|
// Check to see if terms need accepting
|
||||||
this.checkTerms();
|
this.checkTerms();
|
||||||
|
@ -195,7 +196,7 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
|
||||||
try {
|
try {
|
||||||
threepids = await getThreepidsWithBindStatus(cli);
|
threepids = await getThreepidsWithBindStatus(cli);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const idServerUrl = this.context.getIdentityServerUrl();
|
const idServerUrl = cli.getIdentityServerUrl();
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`Unable to reach identity server at ${idServerUrl} to check ` + `for 3PIDs bindings in Settings`,
|
`Unable to reach identity server at ${idServerUrl} to check ` + `for 3PIDs bindings in Settings`,
|
||||||
);
|
);
|
||||||
|
@ -211,7 +212,7 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
|
||||||
private async checkTerms(): Promise<void> {
|
private async checkTerms(): Promise<void> {
|
||||||
// By starting the terms flow we get the logic for checking which terms the user has signed
|
// By starting the terms flow we get the logic for checking which terms the user has signed
|
||||||
// for free. So we might as well use that for our own purposes.
|
// for free. So we might as well use that for our own purposes.
|
||||||
const idServerUrl = this.context.getIdentityServerUrl();
|
const idServerUrl = this.context.client!.getIdentityServerUrl();
|
||||||
if (!this.state.haveIdServer || !idServerUrl) {
|
if (!this.state.haveIdServer || !idServerUrl) {
|
||||||
this.setState({ idServerHasUnsignedTerms: false });
|
this.setState({ idServerHasUnsignedTerms: false });
|
||||||
return;
|
return;
|
||||||
|
@ -221,7 +222,7 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
|
||||||
try {
|
try {
|
||||||
const idAccessToken = await authClient.getAccessToken({ check: false });
|
const idAccessToken = await authClient.getAccessToken({ check: false });
|
||||||
await startTermsFlow(
|
await startTermsFlow(
|
||||||
this.context,
|
this.context.client!,
|
||||||
[new Service(SERVICE_TYPES.IS, idServerUrl, idAccessToken!)],
|
[new Service(SERVICE_TYPES.IS, idServerUrl, idAccessToken!)],
|
||||||
(policiesAndServices, agreedUrls, extraClassNames) => {
|
(policiesAndServices, agreedUrls, extraClassNames) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|
|
@ -19,7 +19,6 @@ import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
import { _t } from "../../../../../languageHandler";
|
import { _t } from "../../../../../languageHandler";
|
||||||
import MatrixClientContext from "../../../../../contexts/MatrixClientContext";
|
|
||||||
import Modal from "../../../../../Modal";
|
import Modal from "../../../../../Modal";
|
||||||
import SettingsSubsection from "../../shared/SettingsSubsection";
|
import SettingsSubsection from "../../shared/SettingsSubsection";
|
||||||
import SetupEncryptionDialog from "../../../dialogs/security/SetupEncryptionDialog";
|
import SetupEncryptionDialog from "../../../dialogs/security/SetupEncryptionDialog";
|
||||||
|
@ -39,8 +38,8 @@ import QuestionDialog from "../../../dialogs/QuestionDialog";
|
||||||
import { FilterVariation } from "../../devices/filter";
|
import { FilterVariation } from "../../devices/filter";
|
||||||
import { OtherSessionsSectionHeading } from "../../devices/OtherSessionsSectionHeading";
|
import { OtherSessionsSectionHeading } from "../../devices/OtherSessionsSectionHeading";
|
||||||
import { SettingsSection } from "../../shared/SettingsSection";
|
import { SettingsSection } from "../../shared/SettingsSection";
|
||||||
import { getDelegatedAuthAccountUrl } from "../../../../../utils/oidc/getDelegatedAuthAccountUrl";
|
|
||||||
import { OidcLogoutDialog } from "../../../dialogs/oidc/OidcLogoutDialog";
|
import { OidcLogoutDialog } from "../../../dialogs/oidc/OidcLogoutDialog";
|
||||||
|
import { SDKContext } from "../../../../../contexts/SDKContext";
|
||||||
|
|
||||||
const confirmSignOut = async (sessionsToSignOutCount: number): Promise<boolean> => {
|
const confirmSignOut = async (sessionsToSignOutCount: number): Promise<boolean> => {
|
||||||
const { finished } = Modal.createDialog(QuestionDialog, {
|
const { finished } = Modal.createDialog(QuestionDialog, {
|
||||||
|
@ -167,13 +166,14 @@ const SessionManagerTab: React.FC = () => {
|
||||||
const filteredDeviceListRef = useRef<HTMLDivElement>(null);
|
const filteredDeviceListRef = useRef<HTMLDivElement>(null);
|
||||||
const scrollIntoViewTimeoutRef = useRef<number>();
|
const scrollIntoViewTimeoutRef = useRef<number>();
|
||||||
|
|
||||||
const matrixClient = useContext(MatrixClientContext);
|
const sdkContext = useContext(SDKContext);
|
||||||
|
const matrixClient = sdkContext.client!;
|
||||||
/**
|
/**
|
||||||
* If we have a delegated auth account management URL, all sessions but the current session need to be managed in the
|
* If we have a delegated auth account management URL, all sessions but the current session need to be managed in the
|
||||||
* delegated auth provider.
|
* delegated auth provider.
|
||||||
* See https://github.com/matrix-org/matrix-spec-proposals/pull/3824
|
* See https://github.com/matrix-org/matrix-spec-proposals/pull/3824
|
||||||
*/
|
*/
|
||||||
const delegatedAuthAccountUrl = getDelegatedAuthAccountUrl(matrixClient.getClientWellKnown());
|
const delegatedAuthAccountUrl = sdkContext.oidcClientStore.accountManagementEndpoint;
|
||||||
const disableMultipleSignout = !!delegatedAuthAccountUrl;
|
const disableMultipleSignout = !!delegatedAuthAccountUrl;
|
||||||
|
|
||||||
const userId = matrixClient?.getUserId();
|
const userId = matrixClient?.getUserId();
|
||||||
|
|
|
@ -1,27 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2023 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { IClientWellKnown, IDelegatedAuthConfig, M_AUTHENTICATION } from "matrix-js-sdk/src/matrix";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the delegated auth account management url if configured
|
|
||||||
* @param clientWellKnown from MatrixClient.getClientWellKnown
|
|
||||||
* @returns the account management url, or undefined
|
|
||||||
*/
|
|
||||||
export const getDelegatedAuthAccountUrl = (clientWellKnown: IClientWellKnown | undefined): string | undefined => {
|
|
||||||
const delegatedAuthConfig = M_AUTHENTICATION.findIn<IDelegatedAuthConfig | undefined>(clientWellKnown);
|
|
||||||
return delegatedAuthConfig?.account;
|
|
||||||
};
|
|
|
@ -16,7 +16,8 @@ limitations under the License.
|
||||||
|
|
||||||
import React, { ReactElement } from "react";
|
import React, { ReactElement } from "react";
|
||||||
import { render } from "@testing-library/react";
|
import { render } from "@testing-library/react";
|
||||||
import { mocked } from "jest-mock";
|
import { mocked, MockedObject } from "jest-mock";
|
||||||
|
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import SettingsStore, { CallbackFn } from "../../../../src/settings/SettingsStore";
|
import SettingsStore, { CallbackFn } from "../../../../src/settings/SettingsStore";
|
||||||
import SdkConfig from "../../../../src/SdkConfig";
|
import SdkConfig from "../../../../src/SdkConfig";
|
||||||
|
@ -30,6 +31,7 @@ import {
|
||||||
} from "../../../test-utils";
|
} from "../../../test-utils";
|
||||||
import { UIFeature } from "../../../../src/settings/UIFeature";
|
import { UIFeature } from "../../../../src/settings/UIFeature";
|
||||||
import { SettingLevel } from "../../../../src/settings/SettingLevel";
|
import { SettingLevel } from "../../../../src/settings/SettingLevel";
|
||||||
|
import { SdkContextClass } from "../../../../src/contexts/SDKContext";
|
||||||
|
|
||||||
mockPlatformPeg({
|
mockPlatformPeg({
|
||||||
supportsSpellCheckSettings: jest.fn().mockReturnValue(false),
|
supportsSpellCheckSettings: jest.fn().mockReturnValue(false),
|
||||||
|
@ -55,18 +57,22 @@ describe("<UserSettingsDialog />", () => {
|
||||||
const userId = "@alice:server.org";
|
const userId = "@alice:server.org";
|
||||||
const mockSettingsStore = mocked(SettingsStore);
|
const mockSettingsStore = mocked(SettingsStore);
|
||||||
const mockSdkConfig = mocked(SdkConfig);
|
const mockSdkConfig = mocked(SdkConfig);
|
||||||
getMockClientWithEventEmitter({
|
let mockClient!: MockedObject<MatrixClient>;
|
||||||
...mockClientMethodsUser(userId),
|
|
||||||
...mockClientMethodsServer(),
|
|
||||||
});
|
|
||||||
|
|
||||||
|
let sdkContext: SdkContextClass;
|
||||||
const defaultProps = { onFinished: jest.fn() };
|
const defaultProps = { onFinished: jest.fn() };
|
||||||
const getComponent = (props: Partial<typeof defaultProps & { initialTabId?: UserTab }> = {}): ReactElement => (
|
const getComponent = (props: Partial<typeof defaultProps & { initialTabId?: UserTab }> = {}): ReactElement => (
|
||||||
<UserSettingsDialog {...defaultProps} {...props} />
|
<UserSettingsDialog sdkContext={sdkContext} {...defaultProps} {...props} />
|
||||||
);
|
);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
|
mockClient = getMockClientWithEventEmitter({
|
||||||
|
...mockClientMethodsUser(userId),
|
||||||
|
...mockClientMethodsServer(),
|
||||||
|
});
|
||||||
|
sdkContext = new SdkContextClass();
|
||||||
|
sdkContext.client = mockClient;
|
||||||
mockSettingsStore.getValue.mockReturnValue(false);
|
mockSettingsStore.getValue.mockReturnValue(false);
|
||||||
mockSettingsStore.getFeatureSettingNames.mockReturnValue([]);
|
mockSettingsStore.getFeatureSettingNames.mockReturnValue([]);
|
||||||
mockSdkConfig.get.mockReturnValue({ brand: "Test" });
|
mockSdkConfig.get.mockReturnValue({ brand: "Test" });
|
||||||
|
|
|
@ -13,11 +13,11 @@ limitations under the License.
|
||||||
|
|
||||||
import { fireEvent, render, screen, within } from "@testing-library/react";
|
import { fireEvent, render, screen, within } from "@testing-library/react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { M_AUTHENTICATION, ThreepidMedium } from "matrix-js-sdk/src/matrix";
|
import { ThreepidMedium } from "matrix-js-sdk/src/matrix";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
import GeneralUserSettingsTab from "../../../../../../src/components/views/settings/tabs/user/GeneralUserSettingsTab";
|
import GeneralUserSettingsTab from "../../../../../../src/components/views/settings/tabs/user/GeneralUserSettingsTab";
|
||||||
import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext";
|
import { SdkContextClass, SDKContext } from "../../../../../../src/contexts/SDKContext";
|
||||||
import SettingsStore from "../../../../../../src/settings/SettingsStore";
|
import SettingsStore from "../../../../../../src/settings/SettingsStore";
|
||||||
import {
|
import {
|
||||||
getMockClientWithEventEmitter,
|
getMockClientWithEventEmitter,
|
||||||
|
@ -28,6 +28,7 @@ import {
|
||||||
} from "../../../../../test-utils";
|
} from "../../../../../test-utils";
|
||||||
import { UIFeature } from "../../../../../../src/settings/UIFeature";
|
import { UIFeature } from "../../../../../../src/settings/UIFeature";
|
||||||
import { SettingLevel } from "../../../../../../src/settings/SettingLevel";
|
import { SettingLevel } from "../../../../../../src/settings/SettingLevel";
|
||||||
|
import { OidcClientStore } from "../../../../../../src/stores/oidc/OidcClientStore";
|
||||||
|
|
||||||
describe("<GeneralUserSettingsTab />", () => {
|
describe("<GeneralUserSettingsTab />", () => {
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
|
@ -44,19 +45,18 @@ describe("<GeneralUserSettingsTab />", () => {
|
||||||
deleteThreePid: jest.fn(),
|
deleteThreePid: jest.fn(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const getComponent = () => (
|
let stores: SdkContextClass;
|
||||||
<MatrixClientContext.Provider value={mockClient}>
|
|
||||||
<GeneralUserSettingsTab {...defaultProps} />
|
|
||||||
</MatrixClientContext.Provider>
|
|
||||||
);
|
|
||||||
|
|
||||||
const clientWellKnownSpy = jest.spyOn(mockClient, "getClientWellKnown");
|
const getComponent = () => (
|
||||||
|
<SDKContext.Provider value={stores}>
|
||||||
|
<GeneralUserSettingsTab {...defaultProps} />
|
||||||
|
</SDKContext.Provider>
|
||||||
|
);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
|
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
|
||||||
mockPlatformPeg();
|
mockPlatformPeg();
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
clientWellKnownSpy.mockReturnValue({});
|
|
||||||
jest.spyOn(SettingsStore, "getValue").mockRestore();
|
jest.spyOn(SettingsStore, "getValue").mockRestore();
|
||||||
jest.spyOn(logger, "error").mockRestore();
|
jest.spyOn(logger, "error").mockRestore();
|
||||||
|
|
||||||
|
@ -67,6 +67,12 @@ describe("<GeneralUserSettingsTab />", () => {
|
||||||
mockClient.deleteThreePid.mockResolvedValue({
|
mockClient.deleteThreePid.mockResolvedValue({
|
||||||
id_server_unbind_result: "success",
|
id_server_unbind_result: "success",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
stores = new SdkContextClass();
|
||||||
|
stores.client = mockClient;
|
||||||
|
// stub out this store completely to avoid mocking initialisation
|
||||||
|
const mockOidcClientStore = {} as unknown as OidcClientStore;
|
||||||
|
jest.spyOn(stores, "oidcClientStore", "get").mockReturnValue(mockOidcClientStore);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not show account management link when not available", () => {
|
it("does not show account management link when not available", () => {
|
||||||
|
@ -78,12 +84,11 @@ describe("<GeneralUserSettingsTab />", () => {
|
||||||
|
|
||||||
it("show account management link in expected format", async () => {
|
it("show account management link in expected format", async () => {
|
||||||
const accountManagementLink = "https://id.server.org/my-account";
|
const accountManagementLink = "https://id.server.org/my-account";
|
||||||
clientWellKnownSpy.mockReturnValue({
|
const mockOidcClientStore = {
|
||||||
[M_AUTHENTICATION.name]: {
|
accountManagementEndpoint: accountManagementLink,
|
||||||
issuer: "https://id.server.org",
|
} as unknown as OidcClientStore;
|
||||||
account: accountManagementLink,
|
jest.spyOn(stores, "oidcClientStore", "get").mockReturnValue(mockOidcClientStore);
|
||||||
},
|
|
||||||
});
|
|
||||||
const { getByTestId } = render(getComponent());
|
const { getByTestId } = render(getComponent());
|
||||||
|
|
||||||
// wait for well-known call to settle
|
// wait for well-known call to settle
|
||||||
|
@ -167,12 +172,11 @@ describe("<GeneralUserSettingsTab />", () => {
|
||||||
(settingName) => settingName === UIFeature.Deactivate,
|
(settingName) => settingName === UIFeature.Deactivate,
|
||||||
);
|
);
|
||||||
// account is managed externally when we have delegated auth configured
|
// account is managed externally when we have delegated auth configured
|
||||||
mockClient.getClientWellKnown.mockReturnValue({
|
const accountManagementLink = "https://id.server.org/my-account";
|
||||||
[M_AUTHENTICATION.name]: {
|
const mockOidcClientStore = {
|
||||||
issuer: "https://issuer.org",
|
accountManagementEndpoint: accountManagementLink,
|
||||||
account: "https://issuer.org/account",
|
} as unknown as OidcClientStore;
|
||||||
},
|
jest.spyOn(stores, "oidcClientStore", "get").mockReturnValue(mockOidcClientStore);
|
||||||
});
|
|
||||||
render(getComponent());
|
render(getComponent());
|
||||||
|
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
|
@ -32,9 +32,9 @@ import {
|
||||||
CryptoApi,
|
CryptoApi,
|
||||||
DeviceVerificationStatus,
|
DeviceVerificationStatus,
|
||||||
MatrixError,
|
MatrixError,
|
||||||
M_AUTHENTICATION,
|
MatrixClient,
|
||||||
} from "matrix-js-sdk/src/matrix";
|
} from "matrix-js-sdk/src/matrix";
|
||||||
import { mocked } from "jest-mock";
|
import { mocked, MockedObject } from "jest-mock";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
clearAllModals,
|
clearAllModals,
|
||||||
|
@ -45,13 +45,14 @@ import {
|
||||||
mockPlatformPeg,
|
mockPlatformPeg,
|
||||||
} from "../../../../../test-utils";
|
} from "../../../../../test-utils";
|
||||||
import SessionManagerTab from "../../../../../../src/components/views/settings/tabs/user/SessionManagerTab";
|
import SessionManagerTab from "../../../../../../src/components/views/settings/tabs/user/SessionManagerTab";
|
||||||
import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext";
|
|
||||||
import Modal from "../../../../../../src/Modal";
|
import Modal from "../../../../../../src/Modal";
|
||||||
import LogoutDialog from "../../../../../../src/components/views/dialogs/LogoutDialog";
|
import LogoutDialog from "../../../../../../src/components/views/dialogs/LogoutDialog";
|
||||||
import { DeviceSecurityVariation, ExtendedDevice } from "../../../../../../src/components/views/settings/devices/types";
|
import { DeviceSecurityVariation, ExtendedDevice } from "../../../../../../src/components/views/settings/devices/types";
|
||||||
import { INACTIVE_DEVICE_AGE_MS } from "../../../../../../src/components/views/settings/devices/filter";
|
import { INACTIVE_DEVICE_AGE_MS } from "../../../../../../src/components/views/settings/devices/filter";
|
||||||
import SettingsStore from "../../../../../../src/settings/SettingsStore";
|
import SettingsStore from "../../../../../../src/settings/SettingsStore";
|
||||||
import { getClientInformationEventType } from "../../../../../../src/utils/device/clientInformation";
|
import { getClientInformationEventType } from "../../../../../../src/utils/device/clientInformation";
|
||||||
|
import { SDKContext, SdkContextClass } from "../../../../../../src/contexts/SDKContext";
|
||||||
|
import { OidcClientStore } from "../../../../../../src/stores/oidc/OidcClientStore";
|
||||||
|
|
||||||
mockPlatformPeg();
|
mockPlatformPeg();
|
||||||
|
|
||||||
|
@ -91,31 +92,14 @@ describe("<SessionManagerTab />", () => {
|
||||||
requestDeviceVerification: jest.fn().mockResolvedValue(mockVerificationRequest),
|
requestDeviceVerification: jest.fn().mockResolvedValue(mockVerificationRequest),
|
||||||
} as unknown as CryptoApi);
|
} as unknown as CryptoApi);
|
||||||
|
|
||||||
let mockClient = getMockClientWithEventEmitter({
|
let mockClient!: MockedObject<MatrixClient>;
|
||||||
...mockClientMethodsUser(aliceId),
|
let sdkContext: SdkContextClass;
|
||||||
getCrypto: jest.fn().mockReturnValue(mockCrypto),
|
|
||||||
getDevices: jest.fn(),
|
|
||||||
getStoredDevice: jest.fn(),
|
|
||||||
getDeviceId: jest.fn().mockReturnValue(deviceId),
|
|
||||||
deleteMultipleDevices: jest.fn(),
|
|
||||||
generateClientSecret: jest.fn(),
|
|
||||||
setDeviceDetails: jest.fn(),
|
|
||||||
getAccountData: jest.fn(),
|
|
||||||
deleteAccountData: jest.fn(),
|
|
||||||
doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(true),
|
|
||||||
getPushers: jest.fn(),
|
|
||||||
setPusher: jest.fn(),
|
|
||||||
setLocalNotificationSettings: jest.fn(),
|
|
||||||
getVersions: jest.fn().mockResolvedValue({}),
|
|
||||||
getCapabilities: jest.fn().mockResolvedValue({}),
|
|
||||||
getClientWellKnown: jest.fn().mockReturnValue({}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const defaultProps = {};
|
const defaultProps = {};
|
||||||
const getComponent = (props = {}): React.ReactElement => (
|
const getComponent = (props = {}): React.ReactElement => (
|
||||||
<MatrixClientContext.Provider value={mockClient}>
|
<SDKContext.Provider value={sdkContext}>
|
||||||
<SessionManagerTab {...defaultProps} {...props} />
|
<SessionManagerTab {...defaultProps} {...props} />
|
||||||
</MatrixClientContext.Provider>
|
</SDKContext.Provider>
|
||||||
);
|
);
|
||||||
|
|
||||||
const toggleDeviceDetails = (
|
const toggleDeviceDetails = (
|
||||||
|
@ -230,6 +214,9 @@ describe("<SessionManagerTab />", () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
sdkContext = new SdkContextClass();
|
||||||
|
sdkContext.client = mockClient;
|
||||||
|
|
||||||
// @ts-ignore allow delete of non-optional prop
|
// @ts-ignore allow delete of non-optional prop
|
||||||
delete window.location;
|
delete window.location;
|
||||||
// @ts-ignore ugly mocking
|
// @ts-ignore ugly mocking
|
||||||
|
@ -1051,12 +1038,11 @@ describe("<SessionManagerTab />", () => {
|
||||||
|
|
||||||
describe("for an OIDC-aware server", () => {
|
describe("for an OIDC-aware server", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockClient.getClientWellKnown.mockReturnValue({
|
// just do an ugly mock here to avoid mocking initialisation
|
||||||
[M_AUTHENTICATION.name]: {
|
const mockOidcClientStore = {
|
||||||
issuer: "https://issuer.org",
|
accountManagementEndpoint: "https://issuer.org/account",
|
||||||
account: "https://issuer.org/account",
|
} as unknown as OidcClientStore;
|
||||||
},
|
jest.spyOn(sdkContext, "oidcClientStore", "get").mockReturnValue(mockOidcClientStore);
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// signing out the current device works as usual
|
// signing out the current device works as usual
|
||||||
|
|
|
@ -1,61 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2023 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { M_AUTHENTICATION } from "matrix-js-sdk/src/matrix";
|
|
||||||
|
|
||||||
import { getDelegatedAuthAccountUrl } from "../../../src/utils/oidc/getDelegatedAuthAccountUrl";
|
|
||||||
|
|
||||||
describe("getDelegatedAuthAccountUrl()", () => {
|
|
||||||
it("should return undefined when wk is undefined", () => {
|
|
||||||
expect(getDelegatedAuthAccountUrl(undefined)).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return undefined when wk has no authentication config", () => {
|
|
||||||
expect(getDelegatedAuthAccountUrl({})).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return undefined when wk authentication config has no configured account url", () => {
|
|
||||||
expect(
|
|
||||||
getDelegatedAuthAccountUrl({
|
|
||||||
[M_AUTHENTICATION.stable!]: {
|
|
||||||
issuer: "issuer.org",
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return the account url for authentication config using the unstable prefix", () => {
|
|
||||||
expect(
|
|
||||||
getDelegatedAuthAccountUrl({
|
|
||||||
[M_AUTHENTICATION.unstable!]: {
|
|
||||||
issuer: "issuer.org",
|
|
||||||
account: "issuer.org/account",
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
).toEqual("issuer.org/account");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return the account url for authentication config using the stable prefix", () => {
|
|
||||||
expect(
|
|
||||||
getDelegatedAuthAccountUrl({
|
|
||||||
[M_AUTHENTICATION.stable!]: {
|
|
||||||
issuer: "issuer.org",
|
|
||||||
account: "issuer.org/account",
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
).toEqual("issuer.org/account");
|
|
||||||
});
|
|
||||||
});
|
|
Loading…
Add table
Add a link
Reference in a new issue