OIDC settings tweaks (#28787)

* Hide 3pid account settings if account is managed externally

As they would be disabled and just confusing otherwise

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Show manage device button instead of sign out button for other devices in OIDC mode

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Tidy up

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix tests

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
pull/28813/head
Michael Telatynski 2024-12-23 22:35:43 +00:00 committed by GitHub
parent 9d5141cfaa
commit 16d2cccb73
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 143 additions and 200 deletions

View File

@ -19,6 +19,7 @@ ignore.push("/OpenSpotlightPayload.ts");
ignore.push("/PinnedMessageBadge.tsx"); ignore.push("/PinnedMessageBadge.tsx");
ignore.push("/editor/mock.ts"); ignore.push("/editor/mock.ts");
ignore.push("DeviceIsolationModeController.ts"); ignore.push("DeviceIsolationModeController.ts");
ignore.push("urls.ts");
ignore.push("/json.ts"); ignore.push("/json.ts");
ignore.push("/ReleaseAnnouncementStore.ts"); ignore.push("/ReleaseAnnouncementStore.ts");
ignore.push("/WidgetLayoutStore.ts"); ignore.push("/WidgetLayoutStore.ts");

View File

@ -1,66 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React, { useState } from "react";
import { _t } from "../../../../languageHandler";
import BaseDialog from "../BaseDialog";
import { getOidcLogoutUrl } from "../../../../utils/oidc/getOidcLogoutUrl";
import AccessibleButton from "../../elements/AccessibleButton";
export interface OidcLogoutDialogProps {
delegatedAuthAccountUrl: string;
deviceId: string;
onFinished(ok?: boolean): void;
}
/**
* Handle logout of OIDC sessions other than the current session
* - ask for user confirmation to open the delegated auth provider
* - open the auth provider in a new tab
* - wait for the user to return and close the modal, we assume the user has completed sign out of the session in auth provider UI
* and trigger a refresh of the session list
*/
export const OidcLogoutDialog: React.FC<OidcLogoutDialogProps> = ({
delegatedAuthAccountUrl,
deviceId,
onFinished,
}) => {
const [hasOpenedLogoutLink, setHasOpenedLogoutLink] = useState(false);
const logoutUrl = getOidcLogoutUrl(delegatedAuthAccountUrl, deviceId);
return (
<BaseDialog onFinished={onFinished} title={_t("action|sign_out")} contentId="mx_Dialog_content">
<div className="mx_Dialog_content" id="mx_Dialog_content">
{_t("auth|oidc|logout_redirect_warning")}
</div>
<div className="mx_Dialog_buttons">
{hasOpenedLogoutLink ? (
<AccessibleButton kind="primary" onClick={() => onFinished(true)}>
{_t("action|close")}
</AccessibleButton>
) : (
<>
<AccessibleButton kind="secondary" onClick={() => onFinished(false)}>
{_t("action|cancel")}
</AccessibleButton>
<AccessibleButton
element="a"
onClick={() => setHasOpenedLogoutLink(true)}
kind="primary"
href={logoutUrl}
target="_blank"
>
{_t("action|continue")}
</AccessibleButton>
</>
)}
</div>
</BaseDialog>
);
};

View File

@ -34,6 +34,7 @@ interface Props {
onSignOutCurrentDevice: () => void; onSignOutCurrentDevice: () => void;
signOutAllOtherSessions?: () => void; signOutAllOtherSessions?: () => void;
saveDeviceName: (deviceName: string) => Promise<void>; saveDeviceName: (deviceName: string) => Promise<void>;
delegatedAuthAccountUrl?: string;
} }
type CurrentDeviceSectionHeadingProps = Pick< type CurrentDeviceSectionHeadingProps = Pick<
@ -90,6 +91,7 @@ const CurrentDeviceSection: React.FC<Props> = ({
onSignOutCurrentDevice, onSignOutCurrentDevice,
signOutAllOtherSessions, signOutAllOtherSessions,
saveDeviceName, saveDeviceName,
delegatedAuthAccountUrl,
}) => { }) => {
const [isExpanded, setIsExpanded] = useState(false); const [isExpanded, setIsExpanded] = useState(false);
@ -126,6 +128,8 @@ const CurrentDeviceSection: React.FC<Props> = ({
onSignOutDevice={onSignOutCurrentDevice} onSignOutDevice={onSignOutCurrentDevice}
saveDeviceName={saveDeviceName} saveDeviceName={saveDeviceName}
className="mx_CurrentDeviceSection_deviceDetails" className="mx_CurrentDeviceSection_deviceDetails"
delegatedAuthAccountUrl={delegatedAuthAccountUrl}
isCurrentDevice
/> />
) : ( ) : (
<> <>

View File

@ -18,6 +18,7 @@ import ToggleSwitch from "../../elements/ToggleSwitch";
import { DeviceDetailHeading } from "./DeviceDetailHeading"; import { DeviceDetailHeading } from "./DeviceDetailHeading";
import { DeviceVerificationStatusCard } from "./DeviceVerificationStatusCard"; import { DeviceVerificationStatusCard } from "./DeviceVerificationStatusCard";
import { ExtendedDevice } from "./types"; import { ExtendedDevice } from "./types";
import { getManageDeviceUrl } from "../../../../utils/oidc/urls.ts";
interface Props { interface Props {
device: ExtendedDevice; device: ExtendedDevice;
@ -31,6 +32,7 @@ interface Props {
supportsMSC3881?: boolean; supportsMSC3881?: boolean;
className?: string; className?: string;
isCurrentDevice?: boolean; isCurrentDevice?: boolean;
delegatedAuthAccountUrl?: string;
} }
interface MetadataTable { interface MetadataTable {
@ -39,6 +41,22 @@ interface MetadataTable {
values: { label: string; value?: string | React.ReactNode }[]; values: { label: string; value?: string | React.ReactNode }[];
} }
function isPushNotificationsEnabled(pusher?: IPusher, notificationSettings?: LocalNotificationSettings): boolean {
if (pusher) return !!pusher[PUSHER_ENABLED.name];
if (notificationSettings) return !notificationSettings.is_silenced;
return true;
}
function isCheckboxDisabled(
pusher?: IPusher,
notificationSettings?: LocalNotificationSettings,
supportsMSC3881?: boolean,
): boolean {
if (notificationSettings) return false;
if (pusher && !supportsMSC3881) return true;
return false;
}
const DeviceDetails: React.FC<Props> = ({ const DeviceDetails: React.FC<Props> = ({
device, device,
pusher, pusher,
@ -51,6 +69,7 @@ const DeviceDetails: React.FC<Props> = ({
supportsMSC3881, supportsMSC3881,
className, className,
isCurrentDevice, isCurrentDevice,
delegatedAuthAccountUrl,
}) => { }) => {
const metadata: MetadataTable[] = [ const metadata: MetadataTable[] = [
{ {
@ -95,18 +114,6 @@ const DeviceDetails: React.FC<Props> = ({
const showPushNotificationSection = !!pusher || !!localNotificationSettings; const showPushNotificationSection = !!pusher || !!localNotificationSettings;
function isPushNotificationsEnabled(pusher?: IPusher, notificationSettings?: LocalNotificationSettings): boolean {
if (pusher) return !!pusher[PUSHER_ENABLED.name];
if (localNotificationSettings) return !localNotificationSettings.is_silenced;
return true;
}
function isCheckboxDisabled(pusher?: IPusher, notificationSettings?: LocalNotificationSettings): boolean {
if (localNotificationSettings) return false;
if (pusher && !supportsMSC3881) return true;
return false;
}
return ( return (
<div className={classNames("mx_DeviceDetails", className)} data-testid={`device-detail-${device.device_id}`}> <div className={classNames("mx_DeviceDetails", className)} data-testid={`device-detail-${device.device_id}`}>
<section className="mx_DeviceDetails_section"> <section className="mx_DeviceDetails_section">
@ -117,32 +124,34 @@ const DeviceDetails: React.FC<Props> = ({
isCurrentDevice={isCurrentDevice} isCurrentDevice={isCurrentDevice}
/> />
</section> </section>
<section className="mx_DeviceDetails_section"> {!delegatedAuthAccountUrl && (
<p className="mx_DeviceDetails_sectionHeading">{_t("settings|sessions|details_heading")}</p> <section className="mx_DeviceDetails_section">
{metadata.map(({ heading, values, id }, index) => ( <p className="mx_DeviceDetails_sectionHeading">{_t("settings|sessions|details_heading")}</p>
<table {metadata.map(({ heading, values, id }, index) => (
className="mx_DeviceDetails_metadataTable" <table
key={index} className="mx_DeviceDetails_metadataTable"
data-testid={`device-detail-metadata-${id}`} key={index}
> data-testid={`device-detail-metadata-${id}`}
{heading && ( >
<thead> {heading && (
<tr> <thead>
<th>{heading}</th> <tr>
</tr> <th>{heading}</th>
</thead> </tr>
)} </thead>
<tbody> )}
{values.map(({ label, value }) => ( <tbody>
<tr key={label}> {values.map(({ label, value }) => (
<td className="mxDeviceDetails_metadataLabel">{label}</td> <tr key={label}>
<td className="mxDeviceDetails_metadataValue">{value}</td> <td className="mxDeviceDetails_metadataLabel">{label}</td>
</tr> <td className="mxDeviceDetails_metadataValue">{value}</td>
))} </tr>
</tbody> ))}
</table> </tbody>
))} </table>
</section> ))}
</section>
)}
{showPushNotificationSection && ( {showPushNotificationSection && (
<section <section
className="mx_DeviceDetails_section mx_DeviceDetails_pushNotifications" className="mx_DeviceDetails_section mx_DeviceDetails_pushNotifications"
@ -152,7 +161,7 @@ const DeviceDetails: React.FC<Props> = ({
// For backwards compatibility, if `enabled` is missing // For backwards compatibility, if `enabled` is missing
// default to `true` // default to `true`
checked={isPushNotificationsEnabled(pusher, localNotificationSettings)} checked={isPushNotificationsEnabled(pusher, localNotificationSettings)}
disabled={isCheckboxDisabled(pusher, localNotificationSettings)} disabled={isCheckboxDisabled(pusher, localNotificationSettings, supportsMSC3881)}
onChange={(checked) => setPushNotifications?.(device.device_id, checked)} onChange={(checked) => setPushNotifications?.(device.device_id, checked)}
title={_t("settings|sessions|push_toggle")} title={_t("settings|sessions|push_toggle")}
data-testid="device-detail-push-notification-checkbox" data-testid="device-detail-push-notification-checkbox"
@ -166,17 +175,30 @@ const DeviceDetails: React.FC<Props> = ({
</section> </section>
)} )}
<section className="mx_DeviceDetails_section"> <section className="mx_DeviceDetails_section">
<AccessibleButton {delegatedAuthAccountUrl && !isCurrentDevice ? (
onClick={onSignOutDevice} <AccessibleButton
kind="danger_inline" element="a"
disabled={isSigningOut} onClick={null}
data-testid="device-detail-sign-out-cta" kind="link_inline"
> href={getManageDeviceUrl(delegatedAuthAccountUrl, device.device_id)}
<span className="mx_DeviceDetails_signOutButtonContent"> target="_blank"
{_t("settings|sessions|sign_out")} data-testid="device-detail-sign-out-cta"
{isSigningOut && <Spinner w={16} h={16} />} >
</span> <span className="mx_DeviceDetails_signOutButtonContent">{_t("settings|sessions|manage")}</span>
</AccessibleButton> </AccessibleButton>
) : (
<AccessibleButton
onClick={onSignOutDevice}
kind="danger_inline"
disabled={isSigningOut}
data-testid="device-detail-sign-out-cta"
>
<span className="mx_DeviceDetails_signOutButtonContent">
{_t("settings|sessions|sign_out")}
{isSigningOut && <Spinner w={16} h={16} />}
</span>
</AccessibleButton>
)}
</section> </section>
</div> </div>
); );

View File

@ -41,10 +41,12 @@ interface Props {
setSelectedDeviceIds: (deviceIds: ExtendedDevice["device_id"][]) => void; setSelectedDeviceIds: (deviceIds: ExtendedDevice["device_id"][]) => void;
supportsMSC3881?: boolean | undefined; supportsMSC3881?: boolean | undefined;
/** /**
* Only allow sessions to be signed out individually * If the user's account is managed externally then sessions must be signed out individually
* Removes checkboxes and multi selection header * Removes checkboxes and multi selection header
* Removes session info as that can be seen in the account management
* Changes sign out button to be a manage button
*/ */
disableMultipleSignout?: boolean; delegatedAuthAccountUrl?: string;
} }
const isDeviceSelected = ( const isDeviceSelected = (
@ -172,6 +174,7 @@ const DeviceListItem: React.FC<{
setPushNotifications: (deviceId: string, enabled: boolean) => Promise<void>; setPushNotifications: (deviceId: string, enabled: boolean) => Promise<void>;
supportsMSC3881?: boolean | undefined; supportsMSC3881?: boolean | undefined;
isSelectDisabled?: boolean; isSelectDisabled?: boolean;
delegatedAuthAccountUrl?: string;
}> = ({ }> = ({
device, device,
pusher, pusher,
@ -187,6 +190,7 @@ const DeviceListItem: React.FC<{
toggleSelected, toggleSelected,
supportsMSC3881, supportsMSC3881,
isSelectDisabled, isSelectDisabled,
delegatedAuthAccountUrl,
}) => { }) => {
const tileContent = ( const tileContent = (
<> <>
@ -222,6 +226,7 @@ const DeviceListItem: React.FC<{
setPushNotifications={setPushNotifications} setPushNotifications={setPushNotifications}
supportsMSC3881={supportsMSC3881} supportsMSC3881={supportsMSC3881}
className="mx_FilteredDeviceList_deviceDetails" className="mx_FilteredDeviceList_deviceDetails"
delegatedAuthAccountUrl={delegatedAuthAccountUrl}
/> />
)} )}
</li> </li>
@ -250,7 +255,7 @@ export const FilteredDeviceList = forwardRef(
setPushNotifications, setPushNotifications,
setSelectedDeviceIds, setSelectedDeviceIds,
supportsMSC3881, supportsMSC3881,
disableMultipleSignout, delegatedAuthAccountUrl,
}: Props, }: Props,
ref: ForwardedRef<HTMLDivElement>, ref: ForwardedRef<HTMLDivElement>,
) => { ) => {
@ -311,7 +316,7 @@ export const FilteredDeviceList = forwardRef(
selectedDeviceCount={selectedDeviceIds.length} selectedDeviceCount={selectedDeviceIds.length}
isAllSelected={isAllSelected} isAllSelected={isAllSelected}
toggleSelectAll={toggleSelectAll} toggleSelectAll={toggleSelectAll}
isSelectDisabled={disableMultipleSignout} isSelectDisabled={!!delegatedAuthAccountUrl}
> >
{selectedDeviceIds.length ? ( {selectedDeviceIds.length ? (
<> <>
@ -361,7 +366,7 @@ export const FilteredDeviceList = forwardRef(
isExpanded={expandedDeviceIds.includes(device.device_id)} isExpanded={expandedDeviceIds.includes(device.device_id)}
isSigningOut={signingOutDeviceIds.includes(device.device_id)} isSigningOut={signingOutDeviceIds.includes(device.device_id)}
isSelected={isDeviceSelected(device.device_id, selectedDeviceIds)} isSelected={isDeviceSelected(device.device_id, selectedDeviceIds)}
isSelectDisabled={disableMultipleSignout} isSelectDisabled={!!delegatedAuthAccountUrl}
onDeviceExpandToggle={() => onDeviceExpandToggle(device.device_id)} onDeviceExpandToggle={() => onDeviceExpandToggle(device.device_id)}
onSignOutDevice={() => onSignOutDevices([device.device_id])} onSignOutDevice={() => onSignOutDevices([device.device_id])}
saveDeviceName={(deviceName: string) => saveDeviceName(device.device_id, deviceName)} saveDeviceName={(deviceName: string) => saveDeviceName(device.device_id, deviceName)}
@ -373,6 +378,7 @@ export const FilteredDeviceList = forwardRef(
setPushNotifications={setPushNotifications} setPushNotifications={setPushNotifications}
toggleSelected={() => toggleSelection(device.device_id)} toggleSelected={() => toggleSelection(device.device_id)}
supportsMSC3881={supportsMSC3881} supportsMSC3881={supportsMSC3881}
delegatedAuthAccountUrl={delegatedAuthAccountUrl}
/> />
))} ))}
</ol> </ol>

View File

@ -186,7 +186,9 @@ const AccountUserSettingsTab: React.FC<IProps> = ({ closeSettingsFn }) => {
canSetDisplayName={canSetDisplayName} canSetDisplayName={canSetDisplayName}
canSetAvatar={canSetAvatar} canSetAvatar={canSetAvatar}
/> />
<UserPersonalInfoSettings canMake3pidChanges={canMake3pidChanges} /> {(!isAccountManagedExternally || canMake3pidChanges) && (
<UserPersonalInfoSettings canMake3pidChanges={canMake3pidChanges} />
)}
<AccountSection <AccountSection
canChangePassword={canChangePassword} canChangePassword={canChangePassword}
onPasswordChanged={onPasswordChanged} onPasswordChanged={onPasswordChanged}

View File

@ -31,7 +31,7 @@ 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 { OidcLogoutDialog } from "../../../dialogs/oidc/OidcLogoutDialog"; import { getManageDeviceUrl } from "../../../../../utils/oidc/urls.ts";
import { SDKContext } from "../../../../../contexts/SDKContext"; import { SDKContext } from "../../../../../contexts/SDKContext";
import Spinner from "../../../elements/Spinner"; import Spinner from "../../../elements/Spinner";
@ -58,16 +58,6 @@ const confirmSignOut = async (sessionsToSignOutCount: number): Promise<boolean>
return !!confirmed; return !!confirmed;
}; };
const confirmDelegatedAuthSignOut = async (delegatedAuthAccountUrl: string, deviceId: string): Promise<boolean> => {
const { finished } = Modal.createDialog(OidcLogoutDialog, {
deviceId,
delegatedAuthAccountUrl,
});
const [confirmed] = await finished;
return !!confirmed;
};
const useSignOut = ( const useSignOut = (
matrixClient: MatrixClient, matrixClient: MatrixClient,
onSignoutResolvedCallback: () => Promise<void>, onSignoutResolvedCallback: () => Promise<void>,
@ -93,20 +83,10 @@ const useSignOut = (
if (!deviceIds.length) { if (!deviceIds.length) {
return; return;
} }
// we can only sign out exactly one OIDC-aware device at a time
// we should not encounter this
if (delegatedAuthAccountUrl && deviceIds.length !== 1) {
logger.warn("Unexpectedly tried to sign out multiple OIDC-aware devices.");
return;
}
// delegated auth logout flow confirms and signs out together const userConfirmedSignout = await confirmSignOut(deviceIds.length);
// so only confirm if we are NOT doing a delegated auth sign out if (!userConfirmedSignout) {
if (!delegatedAuthAccountUrl) { return;
const userConfirmedSignout = await confirmSignOut(deviceIds.length);
if (!userConfirmedSignout) {
return;
}
} }
let success = false; let success = false;
@ -115,11 +95,8 @@ const useSignOut = (
if (delegatedAuthAccountUrl) { if (delegatedAuthAccountUrl) {
const [deviceId] = deviceIds; const [deviceId] = deviceIds;
try { const url = getManageDeviceUrl(delegatedAuthAccountUrl, deviceId);
success = await confirmDelegatedAuthSignOut(delegatedAuthAccountUrl, deviceId); window.open(url, "_blank");
} catch (error) {
logger.error("Error deleting OIDC-aware sessions", error);
}
} else { } else {
const deferredSuccess = defer<boolean>(); const deferredSuccess = defer<boolean>();
await deleteDevicesWithInteractiveAuth(matrixClient, deviceIds, async (success) => { await deleteDevicesWithInteractiveAuth(matrixClient, deviceIds, async (success) => {
@ -323,6 +300,7 @@ const SessionManagerTab: React.FC<{
onSignOutCurrentDevice={onSignOutCurrentDevice} onSignOutCurrentDevice={onSignOutCurrentDevice}
signOutAllOtherSessions={signOutAllOtherSessions} signOutAllOtherSessions={signOutAllOtherSessions}
otherSessionsCount={otherSessionsCount} otherSessionsCount={otherSessionsCount}
delegatedAuthAccountUrl={delegatedAuthAccountUrl}
/> />
{shouldShowOtherSessions && ( {shouldShowOtherSessions && (
<SettingsSubsection <SettingsSubsection
@ -356,7 +334,7 @@ const SessionManagerTab: React.FC<{
setPushNotifications={setPushNotifications} setPushNotifications={setPushNotifications}
ref={filteredDeviceListRef} ref={filteredDeviceListRef}
supportsMSC3881={supportsMSC3881} supportsMSC3881={supportsMSC3881}
disableMultipleSignout={disableMultipleSignout} delegatedAuthAccountUrl={delegatedAuthAccountUrl}
/> />
</SettingsSubsection> </SettingsSubsection>
)} )}

View File

@ -238,7 +238,6 @@
"oidc": { "oidc": {
"error_title": "We couldn't log you in", "error_title": "We couldn't log you in",
"generic_auth_error": "Something went wrong during authentication. Go to the sign in page and try again.", "generic_auth_error": "Something went wrong during authentication. Go to the sign in page and try again.",
"logout_redirect_warning": "You will be redirected to your server's authentication provider to complete sign out.",
"missing_or_invalid_stored_state": "We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again." "missing_or_invalid_stored_state": "We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again."
}, },
"password_field_keep_going_prompt": "Keep going…", "password_field_keep_going_prompt": "Keep going…",
@ -2821,6 +2820,7 @@
"inactive_sessions_list_description": "Consider signing out from old sessions (%(inactiveAgeDays)s days or older) you don't use anymore.", "inactive_sessions_list_description": "Consider signing out from old sessions (%(inactiveAgeDays)s days or older) you don't use anymore.",
"ip": "IP address", "ip": "IP address",
"last_activity": "Last activity", "last_activity": "Last activity",
"manage": "Manage this session",
"mobile_session": "Mobile session", "mobile_session": "Mobile session",
"n_sessions_selected": { "n_sessions_selected": {
"one": "%(count)s session selected", "one": "%(count)s session selected",

View File

@ -1,20 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
/**
* Create a delegated auth account management URL with logout params as per MSC3824 and MSC2965
* https://github.com/matrix-org/matrix-spec-proposals/blob/hughns/sso-redirect-action/proposals/3824-oidc-aware-clients.md#definition-of-oidc-aware
* https://github.com/sandhose/matrix-doc/blob/msc/sandhose/oidc-discovery/proposals/2965-oidc-discovery.md#account-management-url-parameters
*/
export const getOidcLogoutUrl = (delegatedAuthAccountUrl: string, deviceId: string): string => {
const logoutUrl = new URL(delegatedAuthAccountUrl);
logoutUrl.searchParams.set("action", "session_end");
logoutUrl.searchParams.set("device_id", deviceId);
return logoutUrl.toString();
};

32
src/utils/oidc/urls.ts Normal file
View File

@ -0,0 +1,32 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
enum Action {
Profile = "org.matrix.profile",
SessionsList = "org.matrix.sessions_list",
SessionView = "org.matrix.session_view",
SessionEnd = "org.matrix.session_end",
AccountDeactivate = "org.matrix.account_deactivate",
CrossSigningReset = "org.matrix.cross_signing_reset",
}
const getUrl = (authUrl: string, action: Action): URL => {
const url = new URL(authUrl);
url.searchParams.set("action", action);
return url;
};
/**
* Create a delegated auth account management URL with logout params as per MSC4191
* https://github.com/matrix-org/matrix-spec-proposals/blob/quenting/account-deeplink/proposals/4191-account-deeplink.md#possible-actions
*/
export const getManageDeviceUrl = (delegatedAuthAccountUrl: string, deviceId: string): string => {
const url = getUrl(delegatedAuthAccountUrl, Action.SessionView);
url.searchParams.set("device_id", deviceId);
return url.toString();
};

View File

@ -49,7 +49,7 @@ HTMLCollection [
<p <p
class="mx_DeviceSecurityCard_description" class="mx_DeviceSecurityCard_description"
> >
Verify or sign out from this session for best security and reliability. Verify your current session for enhanced secure messaging.
<div <div
class="mx_AccessibleButton mx_LearnMore_button mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline" class="mx_AccessibleButton mx_LearnMore_button mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
role="button" role="button"

View File

@ -114,7 +114,7 @@ describe("<AccountUserSettingsTab />", () => {
expect(manageAccountLink.getAttribute("href")).toMatch(accountManagementLink); expect(manageAccountLink.getAttribute("href")).toMatch(accountManagementLink);
}); });
describe("deactive account", () => { describe("deactivate account", () => {
it("should not render section when account deactivation feature is disabled", () => { it("should not render section when account deactivation feature is disabled", () => {
jest.spyOn(SettingsStore, "getValue").mockImplementation( jest.spyOn(SettingsStore, "getValue").mockImplementation(
(settingName) => settingName !== UIFeature.Deactivate, (settingName) => settingName !== UIFeature.Deactivate,
@ -198,6 +198,11 @@ describe("<AccountUserSettingsTab />", () => {
describe("3pids", () => { describe("3pids", () => {
beforeEach(() => { beforeEach(() => {
const mockOidcClientStore = {
accountManagementEndpoint: undefined,
} as unknown as OidcClientStore;
jest.spyOn(stores, "oidcClientStore", "get").mockReturnValue(mockOidcClientStore);
mockClient.getCapabilities.mockResolvedValue({ mockClient.getCapabilities.mockResolvedValue({
"m.3pid_changes": { "m.3pid_changes": {
enabled: true, enabled: true,

View File

@ -1234,34 +1234,13 @@ describe("<SessionManagerTab />", () => {
toggleDeviceDetails(getByTestId, alicesMobileDevice.device_id); toggleDeviceDetails(getByTestId, alicesMobileDevice.device_id);
const deviceDetails = getByTestId(`device-detail-${alicesMobileDevice.device_id}`); const deviceDetails = getByTestId(`device-detail-${alicesMobileDevice.device_id}`);
const signOutButton = deviceDetails.querySelector( const manageDeviceButton = deviceDetails.querySelector(
'[data-testid="device-detail-sign-out-cta"]', '[data-testid="device-detail-sign-out-cta"]',
) as Element; ) as Element;
fireEvent.click(signOutButton); expect(manageDeviceButton).toHaveAttribute(
await screen.findByRole("dialog");
expect(
screen.getByText(
"You will be redirected to your server's authentication provider to complete sign out.",
),
).toBeInTheDocument();
// correct link to auth provider
expect(screen.getByText("Continue")).toHaveAttribute(
"href", "href",
`https://issuer.org/account?action=session_end&device_id=${alicesMobileDevice.device_id}`, `https://issuer.org/account?action=org.matrix.session_view&device_id=${alicesMobileDevice.device_id}`,
); );
// go to the link
fireEvent.click(screen.getByText("Continue"));
await flushPromises();
// come back from the link and close the modal
fireEvent.click(screen.getByText("Close"));
await flushPromises();
// devices were refreshed
expect(mockClient.getDevices).toHaveBeenCalled();
}); });
it("does not allow removing multiple devices at once", async () => { it("does not allow removing multiple devices at once", async () => {

View File

@ -169,7 +169,7 @@ exports[`<AccountUserSettingsTab /> 3pids should display 3pid email addresses an
</div> </div>
`; `;
exports[`<AccountUserSettingsTab /> deactive account should render section when account deactivation feature is enabled 1`] = ` exports[`<AccountUserSettingsTab /> deactivate account should render section when account deactivation feature is enabled 1`] = `
<div <div
class="mx_SettingsSection" class="mx_SettingsSection"
> >