Delabs native OIDC support (#28615)

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
pull/28791/head
Michael Telatynski 2024-12-20 13:13:41 +00:00 committed by GitHub
parent b07d10cb23
commit db02f26005
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 21 additions and 81 deletions

View File

@ -30,7 +30,6 @@ import AuthHeader from "../../views/auth/AuthHeader";
import AccessibleButton, { ButtonEvent } from "../../views/elements/AccessibleButton"; import AccessibleButton, { ButtonEvent } from "../../views/elements/AccessibleButton";
import { ValidatedServerConfig } from "../../../utils/ValidatedServerConfig"; import { ValidatedServerConfig } from "../../../utils/ValidatedServerConfig";
import { filterBoolean } from "../../../utils/arrays"; import { filterBoolean } from "../../../utils/arrays";
import { Features } from "../../../settings/Settings";
import { startOidcLogin } from "../../../utils/oidc/authorize"; import { startOidcLogin } from "../../../utils/oidc/authorize";
interface IProps { interface IProps {
@ -90,7 +89,6 @@ type OnPasswordLogin = {
*/ */
export default class LoginComponent extends React.PureComponent<IProps, IState> { export default class LoginComponent extends React.PureComponent<IProps, IState> {
private unmounted = false; private unmounted = false;
private oidcNativeFlowEnabled = false;
private loginLogic!: Login; private loginLogic!: Login;
private readonly stepRendererMap: Record<string, () => ReactNode>; private readonly stepRendererMap: Record<string, () => ReactNode>;
@ -98,9 +96,6 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
public constructor(props: IProps) { public constructor(props: IProps) {
super(props); super(props);
// only set on a config level, so we don't need to watch
this.oidcNativeFlowEnabled = SettingsStore.getValue(Features.OidcNativeFlow);
this.state = { this.state = {
busy: false, busy: false,
errorText: null, errorText: null,
@ -358,10 +353,7 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
const loginLogic = new Login(hsUrl, isUrl, fallbackHsUrl, { const loginLogic = new Login(hsUrl, isUrl, fallbackHsUrl, {
defaultDeviceDisplayName: this.props.defaultDeviceDisplayName, defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
// if native OIDC is enabled in the client pass the server's delegated auth settings delegatedAuthentication: this.props.serverConfig.delegatedAuthentication,
delegatedAuthentication: this.oidcNativeFlowEnabled
? this.props.serverConfig.delegatedAuthentication
: undefined,
}); });
this.loginLogic = loginLogic; this.loginLogic = loginLogic;

View File

@ -44,7 +44,6 @@ import { AuthHeaderDisplay } from "./header/AuthHeaderDisplay";
import { AuthHeaderProvider } from "./header/AuthHeaderProvider"; import { AuthHeaderProvider } from "./header/AuthHeaderProvider";
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import { ValidatedServerConfig } from "../../../utils/ValidatedServerConfig"; import { ValidatedServerConfig } from "../../../utils/ValidatedServerConfig";
import { Features } from "../../../settings/Settings";
import { startOidcLogin } from "../../../utils/oidc/authorize"; import { startOidcLogin } from "../../../utils/oidc/authorize";
const debuglog = (...args: any[]): void => { const debuglog = (...args: any[]): void => {
@ -130,8 +129,6 @@ export default class Registration extends React.Component<IProps, IState> {
private readonly loginLogic: Login; private readonly loginLogic: Login;
// `replaceClient` tracks latest serverConfig to spot when it changes under the async method which fetches flows // `replaceClient` tracks latest serverConfig to spot when it changes under the async method which fetches flows
private latestServerConfig?: ValidatedServerConfig; private latestServerConfig?: ValidatedServerConfig;
// cache value from settings store
private oidcNativeFlowEnabled = false;
public constructor(props: IProps) { public constructor(props: IProps) {
super(props); super(props);
@ -150,14 +147,10 @@ export default class Registration extends React.Component<IProps, IState> {
serverDeadError: "", serverDeadError: "",
}; };
// only set on a config level, so we don't need to watch
this.oidcNativeFlowEnabled = SettingsStore.getValue(Features.OidcNativeFlow);
const { hsUrl, isUrl, delegatedAuthentication } = this.props.serverConfig; const { hsUrl, isUrl, delegatedAuthentication } = this.props.serverConfig;
this.loginLogic = new Login(hsUrl, isUrl, null, { this.loginLogic = new Login(hsUrl, isUrl, null, {
defaultDeviceDisplayName: "Element login check", // We shouldn't ever be used defaultDeviceDisplayName: "Element login check", // We shouldn't ever be used
// if native OIDC is enabled in the client pass the server's delegated auth settings delegatedAuthentication,
delegatedAuthentication: this.oidcNativeFlowEnabled ? delegatedAuthentication : undefined,
}); });
} }
@ -227,10 +220,7 @@ export default class Registration extends React.Component<IProps, IState> {
this.loginLogic.setHomeserverUrl(hsUrl); this.loginLogic.setHomeserverUrl(hsUrl);
this.loginLogic.setIdentityServerUrl(isUrl); this.loginLogic.setIdentityServerUrl(isUrl);
// if native OIDC is enabled in the client pass the server's delegated auth settings this.loginLogic.setDelegatedAuthentication(serverConfig.delegatedAuthentication);
const delegatedAuthentication = this.oidcNativeFlowEnabled ? serverConfig.delegatedAuthentication : undefined;
this.loginLogic.setDelegatedAuthentication(delegatedAuthentication);
let ssoFlow: SSOFlow | undefined; let ssoFlow: SSOFlow | undefined;
let oidcNativeFlow: OidcNativeFlow | undefined; let oidcNativeFlow: OidcNativeFlow | undefined;

View File

@ -1461,8 +1461,6 @@
"notification_settings_beta_caption": "Introducing a simpler way to change your notification settings. Customize your %(brand)s, just the way you like.", "notification_settings_beta_caption": "Introducing a simpler way to change your notification settings. Customize your %(brand)s, just the way you like.",
"notification_settings_beta_title": "Notification Settings", "notification_settings_beta_title": "Notification Settings",
"notifications": "Enable the notifications panel in the room header", "notifications": "Enable the notifications panel in the room header",
"oidc_native_flow": "OIDC native authentication",
"oidc_native_flow_description": "⚠ WARNING: Experimental. Use OIDC native authentication when supported by the server.",
"release_announcement": "Release announcement", "release_announcement": "Release announcement",
"render_reaction_images": "Render custom images in reactions", "render_reaction_images": "Render custom images in reactions",
"render_reaction_images_description": "Sometimes referred to as \"custom emojis\".", "render_reaction_images_description": "Sometimes referred to as \"custom emojis\".",

View File

@ -86,7 +86,6 @@ export enum LabGroup {
export enum Features { export enum Features {
NotificationSettings2 = "feature_notification_settings2", NotificationSettings2 = "feature_notification_settings2",
OidcNativeFlow = "feature_oidc_native_flow",
ReleaseAnnouncement = "feature_release_announcement", ReleaseAnnouncement = "feature_release_announcement",
} }
@ -438,15 +437,6 @@ export const SETTINGS: { [setting: string]: ISetting } = {
shouldWarn: true, shouldWarn: true,
default: false, default: false,
}, },
[Features.OidcNativeFlow]: {
isFeature: true,
labsGroup: LabGroup.Developer,
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG_PRIORITISED,
supportedLevelsAreOrdered: true,
displayName: _td("labs|oidc_native_flow"),
description: _td("labs|oidc_native_flow_description"),
default: false,
},
/** /**
* @deprecated in favor of {@link fontSizeDelta} * @deprecated in favor of {@link fontSizeDelta}
*/ */

View File

@ -19,9 +19,6 @@ import { TestSdkContext } from "../../TestSdkContext";
import defaultDispatcher from "../../../../src/dispatcher/dispatcher"; import defaultDispatcher from "../../../../src/dispatcher/dispatcher";
import LogoutDialog from "../../../../src/components/views/dialogs/LogoutDialog"; import LogoutDialog from "../../../../src/components/views/dialogs/LogoutDialog";
import Modal from "../../../../src/Modal"; import Modal from "../../../../src/Modal";
import SettingsStore from "../../../../src/settings/SettingsStore";
import { Features } from "../../../../src/settings/Settings";
import { SettingLevel } from "../../../../src/settings/SettingLevel";
import { mockOpenIdConfiguration } from "../../../test-utils/oidc"; import { mockOpenIdConfiguration } from "../../../test-utils/oidc";
import { Action } from "../../../../src/dispatcher/actions"; import { Action } from "../../../../src/dispatcher/actions";
import { UserTab } from "../../../../src/components/views/dialogs/UserTab"; import { UserTab } from "../../../../src/components/views/dialogs/UserTab";
@ -137,7 +134,6 @@ describe("<UserMenu>", () => {
isCrossSigningReady: jest.fn().mockResolvedValue(true), isCrossSigningReady: jest.fn().mockResolvedValue(true),
exportSecretsBundle: jest.fn().mockResolvedValue({}), exportSecretsBundle: jest.fn().mockResolvedValue({}),
} as unknown as CryptoApi); } as unknown as CryptoApi);
await SettingsStore.setValue(Features.OidcNativeFlow, null, SettingLevel.DEVICE, true);
const spy = jest.spyOn(defaultDispatcher, "dispatch"); const spy = jest.spyOn(defaultDispatcher, "dispatch");
const UserMenu = wrapInSdkContext(UnwrappedUserMenu, sdkContext); const UserMenu = wrapInSdkContext(UnwrappedUserMenu, sdkContext);

View File

@ -19,7 +19,6 @@ import { mkServerConfig, mockPlatformPeg, unmockPlatformPeg } from "../../../../
import Login from "../../../../../src/components/structures/auth/Login"; import Login from "../../../../../src/components/structures/auth/Login";
import BasePlatform from "../../../../../src/BasePlatform"; import BasePlatform from "../../../../../src/BasePlatform";
import SettingsStore from "../../../../../src/settings/SettingsStore"; import SettingsStore from "../../../../../src/settings/SettingsStore";
import { Features } from "../../../../../src/settings/Settings";
import * as registerClientUtils from "../../../../../src/utils/oidc/registerClient"; import * as registerClientUtils from "../../../../../src/utils/oidc/registerClient";
import { makeDelegatedAuthConfig } from "../../../../test-utils/oidc"; import { makeDelegatedAuthConfig } from "../../../../test-utils/oidc";
@ -371,9 +370,6 @@ describe("Login", function () {
const delegatedAuth = makeDelegatedAuthConfig(issuer); const delegatedAuth = makeDelegatedAuthConfig(issuer);
beforeEach(() => { beforeEach(() => {
jest.spyOn(logger, "error"); jest.spyOn(logger, "error");
jest.spyOn(SettingsStore, "getValue").mockImplementation(
(settingName) => settingName === Features.OidcNativeFlow,
);
}); });
afterEach(() => { afterEach(() => {

View File

@ -22,8 +22,6 @@ import {
} from "../../../../test-utils"; } from "../../../../test-utils";
import Registration from "../../../../../src/components/structures/auth/Registration"; import Registration from "../../../../../src/components/structures/auth/Registration";
import { makeDelegatedAuthConfig } from "../../../../test-utils/oidc"; import { makeDelegatedAuthConfig } from "../../../../test-utils/oidc";
import SettingsStore from "../../../../../src/settings/SettingsStore";
import { Features } from "../../../../../src/settings/Settings";
import { startOidcLogin } from "../../../../../src/utils/oidc/authorize"; import { startOidcLogin } from "../../../../../src/utils/oidc/authorize";
jest.mock("../../../../../src/utils/oidc/authorize", () => ({ jest.mock("../../../../../src/utils/oidc/authorize", () => ({
@ -180,49 +178,29 @@ describe("Registration", function () {
fetchMock.get(authConfig.metadata.jwks_uri!, { keys: [] }); fetchMock.get(authConfig.metadata.jwks_uri!, { keys: [] });
}); });
describe("when oidc native flow is not enabled in settings", () => { it("should display oidc-native continue button", async () => {
beforeEach(() => { const { container } = getComponent(defaultHsUrl, defaultIsUrl, authConfig);
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false); await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
}); // no form
expect(container.querySelector("form")).toBeFalsy();
it("should display user/pass registration form", async () => { expect(await screen.findByText("Continue")).toBeTruthy();
const { container } = getComponent(defaultHsUrl, defaultIsUrl, authConfig);
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
expect(container.querySelector("form")).toBeTruthy();
expect(mockClient.loginFlows).toHaveBeenCalled();
expect(mockClient.registerRequest).toHaveBeenCalled();
});
}); });
describe("when oidc native flow is enabled in settings", () => { it("should start OIDC login flow as registration on button click", async () => {
beforeEach(() => { getComponent(defaultHsUrl, defaultIsUrl, authConfig);
jest.spyOn(SettingsStore, "getValue").mockImplementation((key) => key === Features.OidcNativeFlow); await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
});
it("should display oidc-native continue button", async () => { fireEvent.click(await screen.findByText("Continue"));
const { container } = getComponent(defaultHsUrl, defaultIsUrl, authConfig);
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
// no form
expect(container.querySelector("form")).toBeFalsy();
expect(await screen.findByText("Continue")).toBeTruthy(); expect(startOidcLogin).toHaveBeenCalledWith(
}); authConfig,
clientId,
it("should start OIDC login flow as registration on button click", async () => { defaultHsUrl,
getComponent(defaultHsUrl, defaultIsUrl, authConfig); defaultIsUrl,
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…")); // isRegistration
true,
fireEvent.click(await screen.findByText("Continue")); );
expect(startOidcLogin).toHaveBeenCalledWith(
authConfig,
clientId,
defaultHsUrl,
defaultIsUrl,
// isRegistration
true,
);
});
}); });
describe("when is mobile registeration", () => { describe("when is mobile registeration", () => {