Update sso_redirect_options to work for Native OIDC (#32537)

* Remove long deprecated option `sso_immediate_redirect`

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

* Remove stale experimental comment about Native OIDC support

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

* Extract redirectToSso from loadApp

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

* Fix maintaining deeplink when going via auto sso

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

* Improve error

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

* Update `sso_redirect_options` to work for Native OIDC

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

* Update existing test for log changes

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

* Add tests

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski
2026-02-18 09:49:53 +00:00
committed by GitHub
parent 5417fce489
commit 177bc4dad4
7 changed files with 143 additions and 33 deletions
-2
View File
@@ -134,8 +134,6 @@ export interface IConfigOptions {
logout_redirect_url?: string;
// sso_immediate_redirect is deprecated in favour of sso_redirect_options.immediate
sso_immediate_redirect?: boolean;
sso_redirect_options?: ISsoRedirectOptions;
custom_translations_url?: string;
+1 -1
View File
@@ -118,7 +118,7 @@ export default class Login {
);
return [oidcFlow];
} catch (error) {
logger.error(error);
logger.error("Failed to get oidc native flow", error);
}
}
+1 -12
View File
@@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details.
import { mergeWith } from "lodash";
import { SnakedObject } from "./utils/SnakedObject";
import { type IConfigOptions, type ISsoRedirectOptions } from "./IConfigOptions";
import { type IConfigOptions } from "./IConfigOptions";
import { isObject, objectClone } from "./utils/objects";
import { type DeepReadonly, type Defaultize } from "./@types/common";
@@ -141,14 +141,3 @@ export default class SdkConfig {
SdkConfig.put(mergeConfig(SdkConfig.get(), cfg));
}
}
export function parseSsoRedirectOptions(config: IConfigOptions): ISsoRedirectOptions {
// Ignore deprecated options if the config is using new ones
if (config.sso_redirect_options) return config.sso_redirect_options;
// We can cheat here because the default is false anyways
if (config.sso_immediate_redirect) return { immediate: true };
// Default: do nothing
return {};
}
+44 -14
View File
@@ -14,14 +14,14 @@ Please see LICENSE files in the repository root for full details.
import "matrix-js-sdk/src/browser-index";
import React, { type ReactElement, StrictMode } from "react";
import { logger } from "matrix-js-sdk/src/logger";
import { createClient, AutoDiscovery, type ClientConfig } from "matrix-js-sdk/src/matrix";
import { AutoDiscovery, type ClientConfig } from "matrix-js-sdk/src/matrix";
import { WrapperLifecycle, type WrapperOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/WrapperLifecycle";
import type { QueryDict } from "matrix-js-sdk/src/utils";
import PlatformPeg from "../PlatformPeg";
import AutoDiscoveryUtils from "../utils/AutoDiscoveryUtils";
import * as Lifecycle from "../Lifecycle";
import SdkConfig, { parseSsoRedirectOptions } from "../SdkConfig";
import SdkConfig from "../SdkConfig";
import { type IConfigOptions } from "../IConfigOptions";
import { SnakedObject } from "../utils/SnakedObject";
import MatrixChat from "../components/structures/MatrixChat";
@@ -34,6 +34,8 @@ import { ModuleApi } from "../modules/Api";
import { RoomView } from "../components/structures/RoomView";
import RoomAvatar from "../components/views/avatars/RoomAvatar";
import { ModuleNotificationDecoration } from "../modules/components/ModuleNotificationDecoration";
import Login from "../Login.ts";
import { startOidcLogin } from "../utils/oidc/authorize.ts";
logger.log(`Application is running in ${process.env.NODE_ENV} mode`);
@@ -56,6 +58,35 @@ function onTokenLoginCompleted(): void {
window.history.replaceState(null, "", url.href);
}
async function redirectToSso(config: ValidatedServerConfig): Promise<boolean> {
logger.log("Bypassing app load to redirect to SSO");
try {
const login = new Login(config.hsUrl, config.isUrl, null, {
delegatedAuthentication: config.delegatedAuthentication,
});
const flows = await login.getFlows();
const nativeOidcFlow = flows.find((flow) => "clientId" in flow);
if (nativeOidcFlow && config.delegatedAuthentication) {
await startOidcLogin(config.delegatedAuthentication, nativeOidcFlow.clientId, config.hsUrl, config.isUrl);
return true;
}
const flow = flows.find((flow) => flow.type === "m.login.sso" || flow.type === "m.login.cas");
PlatformPeg.get()!.startSingleSignOn(
login.createTemporaryClient(),
flow?.type === "m.login.cas" ? "cas" : "sso",
`/${getScreenFromLocation(window.location).screen}`,
);
return true;
} catch (e) {
console.error("Error encountered during sso redirect", e);
}
return false;
}
export async function loadApp(fragParams: QueryDict, matrixChatRef: React.Ref<MatrixChat>): Promise<ReactElement> {
// XXX: This lives here because certain components import so many things that importing it in a sensible place (eg.
// the builtins module or init.tsx) causes a circular dependency.
@@ -82,8 +113,8 @@ export async function loadApp(fragParams: QueryDict, matrixChatRef: React.Ref<Ma
// Before we continue, let's see if we're supposed to do an SSO redirect
const [userId] = await Lifecycle.getStoredSessionOwner();
const hasPossibleToken = !!userId;
const isReturningFromSso = !!params.loginToken;
const ssoRedirects = parseSsoRedirectOptions(config);
const isReturningFromSso = !!params.loginToken || (!!params.code && !!params.state);
const ssoRedirects = config.sso_redirect_options || {};
let autoRedirect = ssoRedirects.immediate === true;
// XXX: This path matching is a bit brittle, but better to do it early instead of in the app code.
const isWelcomeOrLanding =
@@ -96,25 +127,24 @@ export async function loadApp(fragParams: QueryDict, matrixChatRef: React.Ref<Ma
if (!autoRedirect && ssoRedirects.on_login_page && isLoginPage) {
autoRedirect = true;
}
if (!hasPossibleToken && !isReturningFromSso && autoRedirect) {
logger.log("Bypassing app load to redirect to SSO");
const tempCli = createClient({
baseUrl: config.validated_server_config!.hsUrl,
idBaseUrl: config.validated_server_config!.isUrl,
});
PlatformPeg.get()!.startSingleSignOn(tempCli, "sso", `/${getScreenFromLocation(window.location).screen}`);
// getInitialScreenAfterLogin has a side effect to write to sessionStorage, perform it before auto-redirect
const initialScreenAfterLogin = getInitialScreenAfterLogin(window.location);
if (!hasPossibleToken && !isReturningFromSso && autoRedirect && config.validated_server_config) {
const redirecting = await redirectToSso(config.validated_server_config);
// We return here because startSingleSignOn() will asynchronously redirect us. We don't
// care to wait for it, and don't want to show any UI while we wait (not even half a welcome
// page). As such, just don't even bother loading the MatrixChat component.
return <React.Fragment />;
if (redirecting) {
return <React.Fragment />;
}
}
const defaultDeviceName =
snakedConfig.get("default_device_display_name") ?? platform?.getDefaultDeviceDisplayName();
const initialScreenAfterLogin = getInitialScreenAfterLogin(window.location);
const wrapperOpts: WrapperOpts = { Wrapper: React.Fragment };
ModuleRunner.instance.invoke(WrapperLifecycle.Wrapper, wrapperOpts);