Support for stable MSC4191 account management action parameter (#31701)
* Support for stable MSC4191 account management action parameter * Pass accountManagementActionsSupported around and refactor name * Iterate * Iterate * Attempt to improve clarity of action fallback * Use name "actions supported" consistently * Update test cases for revised default behaviour
This commit is contained in:
@@ -34,7 +34,8 @@ interface Props {
|
|||||||
onSignOutCurrentDevice: () => void;
|
onSignOutCurrentDevice: () => void;
|
||||||
signOutAllOtherSessions?: () => void;
|
signOutAllOtherSessions?: () => void;
|
||||||
saveDeviceName: (deviceName: string) => Promise<void>;
|
saveDeviceName: (deviceName: string) => Promise<void>;
|
||||||
delegatedAuthAccountUrl?: string;
|
accountManagementEndpoint?: string;
|
||||||
|
accountManagementActionsSupported?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
type CurrentDeviceSectionHeadingProps = Pick<
|
type CurrentDeviceSectionHeadingProps = Pick<
|
||||||
@@ -91,7 +92,8 @@ const CurrentDeviceSection: React.FC<Props> = ({
|
|||||||
onSignOutCurrentDevice,
|
onSignOutCurrentDevice,
|
||||||
signOutAllOtherSessions,
|
signOutAllOtherSessions,
|
||||||
saveDeviceName,
|
saveDeviceName,
|
||||||
delegatedAuthAccountUrl,
|
accountManagementEndpoint,
|
||||||
|
accountManagementActionsSupported,
|
||||||
}) => {
|
}) => {
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
|
||||||
@@ -128,7 +130,8 @@ const CurrentDeviceSection: React.FC<Props> = ({
|
|||||||
onSignOutDevice={onSignOutCurrentDevice}
|
onSignOutDevice={onSignOutCurrentDevice}
|
||||||
saveDeviceName={saveDeviceName}
|
saveDeviceName={saveDeviceName}
|
||||||
className="mx_CurrentDeviceSection_deviceDetails"
|
className="mx_CurrentDeviceSection_deviceDetails"
|
||||||
delegatedAuthAccountUrl={delegatedAuthAccountUrl}
|
accountManagementEndpoint={accountManagementEndpoint}
|
||||||
|
accountManagementActionsSupported={accountManagementActionsSupported}
|
||||||
isCurrentDevice
|
isCurrentDevice
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ interface Props {
|
|||||||
supportsMSC3881?: boolean;
|
supportsMSC3881?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
isCurrentDevice?: boolean;
|
isCurrentDevice?: boolean;
|
||||||
delegatedAuthAccountUrl?: string;
|
accountManagementEndpoint?: string;
|
||||||
|
accountManagementActionsSupported?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MetadataTable {
|
interface MetadataTable {
|
||||||
@@ -69,7 +70,8 @@ const DeviceDetails: React.FC<Props> = ({
|
|||||||
supportsMSC3881,
|
supportsMSC3881,
|
||||||
className,
|
className,
|
||||||
isCurrentDevice,
|
isCurrentDevice,
|
||||||
delegatedAuthAccountUrl,
|
accountManagementEndpoint,
|
||||||
|
accountManagementActionsSupported,
|
||||||
}) => {
|
}) => {
|
||||||
const metadata: MetadataTable[] = [
|
const metadata: MetadataTable[] = [
|
||||||
{
|
{
|
||||||
@@ -124,7 +126,7 @@ const DeviceDetails: React.FC<Props> = ({
|
|||||||
isCurrentDevice={isCurrentDevice}
|
isCurrentDevice={isCurrentDevice}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
{!delegatedAuthAccountUrl && (
|
{!accountManagementEndpoint && (
|
||||||
<section className="mx_DeviceDetails_section">
|
<section className="mx_DeviceDetails_section">
|
||||||
<p className="mx_DeviceDetails_sectionHeading">{_t("settings|sessions|details_heading")}</p>
|
<p className="mx_DeviceDetails_sectionHeading">{_t("settings|sessions|details_heading")}</p>
|
||||||
{metadata.map(({ heading, values, id }, index) => (
|
{metadata.map(({ heading, values, id }, index) => (
|
||||||
@@ -175,12 +177,16 @@ const DeviceDetails: React.FC<Props> = ({
|
|||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
<section className="mx_DeviceDetails_section">
|
<section className="mx_DeviceDetails_section">
|
||||||
{delegatedAuthAccountUrl && !isCurrentDevice ? (
|
{accountManagementEndpoint && !isCurrentDevice ? (
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
element="a"
|
element="a"
|
||||||
onClick={null}
|
onClick={null}
|
||||||
kind="link_inline"
|
kind="link_inline"
|
||||||
href={getManageDeviceUrl(delegatedAuthAccountUrl, device.device_id)}
|
href={getManageDeviceUrl(
|
||||||
|
accountManagementEndpoint,
|
||||||
|
accountManagementActionsSupported,
|
||||||
|
device.device_id,
|
||||||
|
)}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
data-testid="device-detail-sign-out-cta"
|
data-testid="device-detail-sign-out-cta"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -46,7 +46,8 @@ interface Props {
|
|||||||
* Removes session info as that can be seen in the account management
|
* Removes session info as that can be seen in the account management
|
||||||
* Changes sign out button to be a manage button
|
* Changes sign out button to be a manage button
|
||||||
*/
|
*/
|
||||||
delegatedAuthAccountUrl?: string;
|
accountManagementEndpoint?: string;
|
||||||
|
accountManagementActionsSupported?: string[];
|
||||||
ref?: Ref<HTMLDivElement>;
|
ref?: Ref<HTMLDivElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,7 +176,8 @@ 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;
|
accountManagementEndpoint?: string;
|
||||||
|
accountManagementActionsSupported?: string[];
|
||||||
}> = ({
|
}> = ({
|
||||||
device,
|
device,
|
||||||
pusher,
|
pusher,
|
||||||
@@ -191,7 +193,8 @@ const DeviceListItem: React.FC<{
|
|||||||
toggleSelected,
|
toggleSelected,
|
||||||
supportsMSC3881,
|
supportsMSC3881,
|
||||||
isSelectDisabled,
|
isSelectDisabled,
|
||||||
delegatedAuthAccountUrl,
|
accountManagementEndpoint,
|
||||||
|
accountManagementActionsSupported,
|
||||||
}) => {
|
}) => {
|
||||||
const tileContent = (
|
const tileContent = (
|
||||||
<>
|
<>
|
||||||
@@ -227,7 +230,8 @@ const DeviceListItem: React.FC<{
|
|||||||
setPushNotifications={setPushNotifications}
|
setPushNotifications={setPushNotifications}
|
||||||
supportsMSC3881={supportsMSC3881}
|
supportsMSC3881={supportsMSC3881}
|
||||||
className="mx_FilteredDeviceList_deviceDetails"
|
className="mx_FilteredDeviceList_deviceDetails"
|
||||||
delegatedAuthAccountUrl={delegatedAuthAccountUrl}
|
accountManagementEndpoint={accountManagementEndpoint}
|
||||||
|
accountManagementActionsSupported={accountManagementActionsSupported}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
@@ -254,7 +258,8 @@ export const FilteredDeviceList = ({
|
|||||||
setPushNotifications,
|
setPushNotifications,
|
||||||
setSelectedDeviceIds,
|
setSelectedDeviceIds,
|
||||||
supportsMSC3881,
|
supportsMSC3881,
|
||||||
delegatedAuthAccountUrl,
|
accountManagementEndpoint,
|
||||||
|
accountManagementActionsSupported,
|
||||||
ref,
|
ref,
|
||||||
}: Props): JSX.Element => {
|
}: Props): JSX.Element => {
|
||||||
const sortedDevices = getFilteredSortedDevices(devices, filter);
|
const sortedDevices = getFilteredSortedDevices(devices, filter);
|
||||||
@@ -314,7 +319,7 @@ export const FilteredDeviceList = ({
|
|||||||
selectedDeviceCount={selectedDeviceIds.length}
|
selectedDeviceCount={selectedDeviceIds.length}
|
||||||
isAllSelected={isAllSelected}
|
isAllSelected={isAllSelected}
|
||||||
toggleSelectAll={toggleSelectAll}
|
toggleSelectAll={toggleSelectAll}
|
||||||
isSelectDisabled={!!delegatedAuthAccountUrl}
|
isSelectDisabled={!!accountManagementEndpoint}
|
||||||
>
|
>
|
||||||
{selectedDeviceIds.length ? (
|
{selectedDeviceIds.length ? (
|
||||||
<>
|
<>
|
||||||
@@ -364,7 +369,7 @@ export const FilteredDeviceList = ({
|
|||||||
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={!!delegatedAuthAccountUrl}
|
isSelectDisabled={!!accountManagementEndpoint}
|
||||||
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)}
|
||||||
@@ -376,7 +381,8 @@ export const FilteredDeviceList = ({
|
|||||||
setPushNotifications={setPushNotifications}
|
setPushNotifications={setPushNotifications}
|
||||||
toggleSelected={() => toggleSelection(device.device_id)}
|
toggleSelected={() => toggleSelection(device.device_id)}
|
||||||
supportsMSC3881={supportsMSC3881}
|
supportsMSC3881={supportsMSC3881}
|
||||||
delegatedAuthAccountUrl={delegatedAuthAccountUrl}
|
accountManagementEndpoint={accountManagementEndpoint}
|
||||||
|
accountManagementActionsSupported={accountManagementActionsSupported}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</ol>
|
</ol>
|
||||||
|
|||||||
@@ -60,7 +60,8 @@ const confirmSignOut = async (sessionsToSignOutCount: number): Promise<boolean>
|
|||||||
const useSignOut = (
|
const useSignOut = (
|
||||||
matrixClient: MatrixClient,
|
matrixClient: MatrixClient,
|
||||||
onSignoutResolvedCallback: () => Promise<void>,
|
onSignoutResolvedCallback: () => Promise<void>,
|
||||||
delegatedAuthAccountUrl?: string,
|
accountManagementEndpoint?: string,
|
||||||
|
accountManagementActionsSupported?: string[],
|
||||||
): {
|
): {
|
||||||
onSignOutCurrentDevice: () => void;
|
onSignOutCurrentDevice: () => void;
|
||||||
onSignOutOtherDevices: (deviceIds: ExtendedDevice["device_id"][]) => Promise<void>;
|
onSignOutOtherDevices: (deviceIds: ExtendedDevice["device_id"][]) => Promise<void>;
|
||||||
@@ -92,9 +93,9 @@ const useSignOut = (
|
|||||||
try {
|
try {
|
||||||
setSigningOutDeviceIds((signingOutDeviceIds) => [...signingOutDeviceIds, ...deviceIds]);
|
setSigningOutDeviceIds((signingOutDeviceIds) => [...signingOutDeviceIds, ...deviceIds]);
|
||||||
|
|
||||||
if (delegatedAuthAccountUrl) {
|
if (accountManagementEndpoint) {
|
||||||
const [deviceId] = deviceIds;
|
const [deviceId] = deviceIds;
|
||||||
const url = getManageDeviceUrl(delegatedAuthAccountUrl, deviceId);
|
const url = getManageDeviceUrl(accountManagementEndpoint, accountManagementActionsSupported, deviceId);
|
||||||
window.open(url, "_blank");
|
window.open(url, "_blank");
|
||||||
} else {
|
} else {
|
||||||
const deferredSuccess = Promise.withResolvers<boolean>();
|
const deferredSuccess = Promise.withResolvers<boolean>();
|
||||||
@@ -151,12 +152,14 @@ const SessionManagerTab: React.FC<{
|
|||||||
* 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 = useAsyncMemo(async () => {
|
const accountManagement = useAsyncMemo(async () => {
|
||||||
await sdkContext.oidcClientStore.readyPromise; // wait for the store to be ready
|
await sdkContext.oidcClientStore.readyPromise; // wait for the store to be ready
|
||||||
return sdkContext.oidcClientStore.accountManagementEndpoint;
|
return {
|
||||||
|
endpoint: sdkContext.oidcClientStore.accountManagementEndpoint,
|
||||||
|
actionsSupported: sdkContext.oidcClientStore.accountManagementActionsSupported,
|
||||||
|
};
|
||||||
}, [sdkContext.oidcClientStore]);
|
}, [sdkContext.oidcClientStore]);
|
||||||
const disableMultipleSignout = !!delegatedAuthAccountUrl;
|
const disableMultipleSignout = !!accountManagement?.endpoint;
|
||||||
|
|
||||||
const userId = matrixClient?.getUserId();
|
const userId = matrixClient?.getUserId();
|
||||||
const currentUserMember = (userId && matrixClient?.getUser(userId)) || undefined;
|
const currentUserMember = (userId && matrixClient?.getUser(userId)) || undefined;
|
||||||
const clientVersions = useAsyncMemo(() => matrixClient.getVersions(), [matrixClient]);
|
const clientVersions = useAsyncMemo(() => matrixClient.getVersions(), [matrixClient]);
|
||||||
@@ -232,7 +235,8 @@ const SessionManagerTab: React.FC<{
|
|||||||
const { onSignOutCurrentDevice, onSignOutOtherDevices, signingOutDeviceIds } = useSignOut(
|
const { onSignOutCurrentDevice, onSignOutOtherDevices, signingOutDeviceIds } = useSignOut(
|
||||||
matrixClient,
|
matrixClient,
|
||||||
onSignoutResolvedCallback,
|
onSignoutResolvedCallback,
|
||||||
delegatedAuthAccountUrl,
|
accountManagement?.endpoint,
|
||||||
|
accountManagement?.actionsSupported,
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
@@ -297,7 +301,8 @@ const SessionManagerTab: React.FC<{
|
|||||||
onSignOutCurrentDevice={onSignOutCurrentDevice}
|
onSignOutCurrentDevice={onSignOutCurrentDevice}
|
||||||
signOutAllOtherSessions={signOutAllOtherSessions}
|
signOutAllOtherSessions={signOutAllOtherSessions}
|
||||||
otherSessionsCount={otherSessionsCount}
|
otherSessionsCount={otherSessionsCount}
|
||||||
delegatedAuthAccountUrl={delegatedAuthAccountUrl}
|
accountManagementEndpoint={accountManagement?.endpoint}
|
||||||
|
accountManagementActionsSupported={accountManagement?.actionsSupported}
|
||||||
/>
|
/>
|
||||||
{shouldShowOtherSessions && (
|
{shouldShowOtherSessions && (
|
||||||
<SettingsSubsection
|
<SettingsSubsection
|
||||||
@@ -331,7 +336,8 @@ const SessionManagerTab: React.FC<{
|
|||||||
setPushNotifications={setPushNotifications}
|
setPushNotifications={setPushNotifications}
|
||||||
ref={filteredDeviceListRef}
|
ref={filteredDeviceListRef}
|
||||||
supportsMSC3881={supportsMSC3881}
|
supportsMSC3881={supportsMSC3881}
|
||||||
delegatedAuthAccountUrl={delegatedAuthAccountUrl}
|
accountManagementEndpoint={accountManagement?.endpoint}
|
||||||
|
accountManagementActionsSupported={accountManagement?.actionsSupported}
|
||||||
/>
|
/>
|
||||||
</SettingsSubsection>
|
</SettingsSubsection>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export class OidcClientStore {
|
|||||||
private initialisingOidcClientPromise: Promise<void> | undefined;
|
private initialisingOidcClientPromise: Promise<void> | undefined;
|
||||||
private authenticatedIssuer?: string; // set only in OIDC-native mode
|
private authenticatedIssuer?: string; // set only in OIDC-native mode
|
||||||
private _accountManagementEndpoint?: string;
|
private _accountManagementEndpoint?: string;
|
||||||
|
private _accountManagementActionsSupported?: string[];
|
||||||
/**
|
/**
|
||||||
* Promise which resolves once this store is read to use, which may mean there is no OIDC client if we're in legacy mode,
|
* Promise which resolves once this store is read to use, which may mean there is no OIDC client if we're in legacy mode,
|
||||||
* or we just have the account management endpoint if running in OIDC-aware mode.
|
* or we just have the account management endpoint if running in OIDC-aware mode.
|
||||||
@@ -51,7 +52,11 @@ export class OidcClientStore {
|
|||||||
// We are not in OIDC Native mode, as we have no locally stored issuer. Check if the server delegates auth to OIDC.
|
// We are not in OIDC Native mode, as we have no locally stored issuer. Check if the server delegates auth to OIDC.
|
||||||
try {
|
try {
|
||||||
const authMetadata = await this.matrixClient.getAuthMetadata();
|
const authMetadata = await this.matrixClient.getAuthMetadata();
|
||||||
this.setAccountManagementEndpoint(authMetadata.account_management_uri, authMetadata.issuer);
|
this.setAccountManagementEndpoint(
|
||||||
|
authMetadata.account_management_uri,
|
||||||
|
authMetadata.issuer,
|
||||||
|
authMetadata.account_management_actions_supported,
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("Auth issuer not found", e);
|
console.log("Auth issuer not found", e);
|
||||||
}
|
}
|
||||||
@@ -65,7 +70,11 @@ export class OidcClientStore {
|
|||||||
return !!this.authenticatedIssuer;
|
return !!this.authenticatedIssuer;
|
||||||
}
|
}
|
||||||
|
|
||||||
private setAccountManagementEndpoint(endpoint: string | undefined, issuer: string): void {
|
private setAccountManagementEndpoint(
|
||||||
|
endpoint: string | undefined,
|
||||||
|
issuer: string,
|
||||||
|
actionsSupported?: string[],
|
||||||
|
): void {
|
||||||
// if no account endpoint is configured default to the issuer
|
// if no account endpoint is configured default to the issuer
|
||||||
const url = new URL(endpoint ?? issuer);
|
const url = new URL(endpoint ?? issuer);
|
||||||
const idToken = getStoredOidcIdToken();
|
const idToken = getStoredOidcIdToken();
|
||||||
@@ -73,12 +82,17 @@ export class OidcClientStore {
|
|||||||
url.searchParams.set("id_token_hint", idToken);
|
url.searchParams.set("id_token_hint", idToken);
|
||||||
}
|
}
|
||||||
this._accountManagementEndpoint = url.toString();
|
this._accountManagementEndpoint = url.toString();
|
||||||
|
this._accountManagementActionsSupported = actionsSupported;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get accountManagementEndpoint(): string | undefined {
|
public get accountManagementEndpoint(): string | undefined {
|
||||||
return this._accountManagementEndpoint;
|
return this._accountManagementEndpoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get accountManagementActionsSupported(): string[] | undefined {
|
||||||
|
return this._accountManagementActionsSupported;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Revokes provided access and refresh tokens with the configured OIDC provider
|
* Revokes provided access and refresh tokens with the configured OIDC provider
|
||||||
* @param accessToken
|
* @param accessToken
|
||||||
@@ -151,7 +165,11 @@ export class OidcClientStore {
|
|||||||
try {
|
try {
|
||||||
const clientId = getStoredOidcClientId();
|
const clientId = getStoredOidcClientId();
|
||||||
const authMetadata = await discoverAndValidateOIDCIssuerWellKnown(this.authenticatedIssuer);
|
const authMetadata = await discoverAndValidateOIDCIssuerWellKnown(this.authenticatedIssuer);
|
||||||
this.setAccountManagementEndpoint(authMetadata.account_management_uri, authMetadata.issuer);
|
this.setAccountManagementEndpoint(
|
||||||
|
authMetadata.account_management_uri,
|
||||||
|
authMetadata.issuer,
|
||||||
|
authMetadata.account_management_actions_supported,
|
||||||
|
);
|
||||||
this.oidcClient = new OidcClient({
|
this.oidcClient = new OidcClient({
|
||||||
authority: authMetadata.issuer,
|
authority: authMetadata.issuer,
|
||||||
signingKeys: authMetadata.signingKeys ?? undefined,
|
signingKeys: authMetadata.signingKeys ?? undefined,
|
||||||
|
|||||||
+35
-6
@@ -1,4 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
|
Copyright 2026 Element Creations Ltd.
|
||||||
Copyright 2024 New Vector Ltd.
|
Copyright 2024 New Vector Ltd.
|
||||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
@@ -8,14 +9,14 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
|
|
||||||
enum Action {
|
enum Action {
|
||||||
Profile = "org.matrix.profile",
|
Profile = "org.matrix.profile",
|
||||||
SessionsList = "org.matrix.sessions_list",
|
DevicesList = "org.matrix.devices_list",
|
||||||
SessionView = "org.matrix.session_view",
|
DeviceView = "org.matrix.device_view",
|
||||||
SessionEnd = "org.matrix.session_end",
|
DeviceDelete = "org.matrix.device_delete",
|
||||||
AccountDeactivate = "org.matrix.account_deactivate",
|
AccountDeactivate = "org.matrix.account_deactivate",
|
||||||
CrossSigningReset = "org.matrix.cross_signing_reset",
|
CrossSigningReset = "org.matrix.cross_signing_reset",
|
||||||
}
|
}
|
||||||
|
|
||||||
const getUrl = (authUrl: string, action: Action): URL => {
|
const getUrl = (authUrl: string, action: Action | string): URL => {
|
||||||
const url = new URL(authUrl);
|
const url = new URL(authUrl);
|
||||||
url.searchParams.set("action", action);
|
url.searchParams.set("action", action);
|
||||||
return url;
|
return url;
|
||||||
@@ -25,8 +26,36 @@ const getUrl = (authUrl: string, action: Action): URL => {
|
|||||||
* Create a delegated auth account management URL with logout params as per MSC4191
|
* 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
|
* 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 => {
|
export const getManageDeviceUrl = (
|
||||||
const url = getUrl(delegatedAuthAccountUrl, Action.SessionView);
|
accountManagementEndpoint: string,
|
||||||
|
accountManagementActionsSupported: string[] | undefined,
|
||||||
|
deviceId: string,
|
||||||
|
): string => {
|
||||||
|
let action: string | undefined;
|
||||||
|
|
||||||
|
// pick the action= parameter that the server supports:
|
||||||
|
if (accountManagementActionsSupported?.includes(Action.DeviceView)) {
|
||||||
|
// stable action
|
||||||
|
action = Action.DeviceView;
|
||||||
|
} else if (accountManagementActionsSupported?.includes("org.matrix.session_view")) {
|
||||||
|
// unstable action from earlier version of MSC4191, can be removed once stable is widely supported
|
||||||
|
action = "org.matrix.session_view";
|
||||||
|
} else if (accountManagementActionsSupported?.includes("session_view")) {
|
||||||
|
// unstable action from earlier version of MSC4191, can be removed once stable is widely supported
|
||||||
|
action = "session_view";
|
||||||
|
}
|
||||||
|
if (!action) {
|
||||||
|
if (accountManagementActionsSupported) {
|
||||||
|
// the server gave a list of supported actions, but none we know about:
|
||||||
|
// send the stable action anyway
|
||||||
|
action = Action.DeviceView;
|
||||||
|
} else {
|
||||||
|
// the server did not provide a list of supported actions:
|
||||||
|
// to be backwards compatible, use the value that we used to always send
|
||||||
|
action = "org.matrix.session_view";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const url = getUrl(accountManagementEndpoint, action);
|
||||||
url.searchParams.set("device_id", deviceId);
|
url.searchParams.set("device_id", deviceId);
|
||||||
return url.toString();
|
return url.toString();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ describe("OidcClientStore", () => {
|
|||||||
const clientId = "test-client-id";
|
const clientId = "test-client-id";
|
||||||
const authConfig = makeDelegatedAuthConfig();
|
const authConfig = makeDelegatedAuthConfig();
|
||||||
const account = authConfig.issuer + "account";
|
const account = authConfig.issuer + "account";
|
||||||
|
const accountManagementActionsSupported = ["action1", "action2"];
|
||||||
|
|
||||||
const mockClient = getMockClientWithEventEmitter({
|
const mockClient = getMockClientWithEventEmitter({
|
||||||
getAuthMetadata: jest.fn(),
|
getAuthMetadata: jest.fn(),
|
||||||
@@ -41,6 +42,7 @@ describe("OidcClientStore", () => {
|
|||||||
.mockResolvedValue({
|
.mockResolvedValue({
|
||||||
...authConfig,
|
...authConfig,
|
||||||
account_management_uri: account,
|
account_management_uri: account,
|
||||||
|
account_management_actions_supported: accountManagementActionsSupported,
|
||||||
authorization_endpoint: "authorization-endpoint",
|
authorization_endpoint: "authorization-endpoint",
|
||||||
token_endpoint: "token-endpoint",
|
token_endpoint: "token-endpoint",
|
||||||
});
|
});
|
||||||
@@ -130,6 +132,15 @@ describe("OidcClientStore", () => {
|
|||||||
expect(store.accountManagementEndpoint).toEqual(account);
|
expect(store.accountManagementEndpoint).toEqual(account);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should set account management actions supported when configured", async () => {
|
||||||
|
const store = new OidcClientStore(mockClient);
|
||||||
|
|
||||||
|
// @ts-ignore private property
|
||||||
|
await store.getOidcClient();
|
||||||
|
|
||||||
|
expect(store.accountManagementActionsSupported).toEqual(accountManagementActionsSupported);
|
||||||
|
});
|
||||||
|
|
||||||
it("should set account management endpoint to issuer when not configured", async () => {
|
it("should set account management endpoint to issuer when not configured", async () => {
|
||||||
mocked(discoverAndValidateOIDCIssuerWellKnown)
|
mocked(discoverAndValidateOIDCIssuerWellKnown)
|
||||||
.mockClear()
|
.mockClear()
|
||||||
@@ -244,10 +255,12 @@ describe("OidcClientStore", () => {
|
|||||||
mockClient.getAuthMetadata.mockResolvedValue({
|
mockClient.getAuthMetadata.mockResolvedValue({
|
||||||
...authConfig,
|
...authConfig,
|
||||||
account_management_uri: account,
|
account_management_uri: account,
|
||||||
|
account_management_actions_supported: accountManagementActionsSupported,
|
||||||
});
|
});
|
||||||
const store = new OidcClientStore(mockClient);
|
const store = new OidcClientStore(mockClient);
|
||||||
await store.readyPromise;
|
await store.readyPromise;
|
||||||
expect(store.accountManagementEndpoint).toBe(account);
|
expect(store.accountManagementEndpoint).toBe(account);
|
||||||
|
expect(store.accountManagementActionsSupported).toEqual(accountManagementActionsSupported);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 Element Creations Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getManageDeviceUrl } from "../../../../src/utils/oidc/urls";
|
||||||
|
|
||||||
|
describe("OIDC urls", () => {
|
||||||
|
const accountManagementEndpoint = "https://auth.com/manage";
|
||||||
|
const deviceId = "DEVICEID1234";
|
||||||
|
|
||||||
|
describe("getManageDeviceUrl()", () => {
|
||||||
|
it("prefers stable action", async () => {
|
||||||
|
expect(
|
||||||
|
getManageDeviceUrl(
|
||||||
|
accountManagementEndpoint,
|
||||||
|
["org.matrix.session_view", "session_view", "org.matrix.device_view"],
|
||||||
|
deviceId,
|
||||||
|
),
|
||||||
|
).toEqual("https://auth.com/manage?action=org.matrix.device_view&device_id=DEVICEID1234");
|
||||||
|
});
|
||||||
|
it("defaults to stable action when no known action is supported", async () => {
|
||||||
|
expect(getManageDeviceUrl(accountManagementEndpoint, [], deviceId)).toEqual(
|
||||||
|
"https://auth.com/manage?action=org.matrix.device_view&device_id=DEVICEID1234",
|
||||||
|
);
|
||||||
|
expect(getManageDeviceUrl(accountManagementEndpoint, ["foo"], deviceId)).toEqual(
|
||||||
|
"https://auth.com/manage?action=org.matrix.device_view&device_id=DEVICEID1234",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it("defaults to backwards compatible action when no supported actions are provided", async () => {
|
||||||
|
expect(getManageDeviceUrl(accountManagementEndpoint, undefined, deviceId)).toEqual(
|
||||||
|
"https://auth.com/manage?action=org.matrix.session_view&device_id=DEVICEID1234",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it("uses unstable org.matrix.session_view", async () => {
|
||||||
|
expect(getManageDeviceUrl(accountManagementEndpoint, ["org.matrix.session_view"], deviceId)).toEqual(
|
||||||
|
"https://auth.com/manage?action=org.matrix.session_view&device_id=DEVICEID1234",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it("uses unstable session_view", async () => {
|
||||||
|
expect(getManageDeviceUrl(accountManagementEndpoint, ["session_view"], deviceId)).toEqual(
|
||||||
|
"https://auth.com/manage?action=session_view&device_id=DEVICEID1234",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user