Skip to content

Commit

Permalink
Merge branch 'develop' into t3chguy/oidc-tweaks
Browse files Browse the repository at this point in the history
  • Loading branch information
t3chguy authored Dec 23, 2024
2 parents e1a8267 + 9d5141c commit 08373c0
Show file tree
Hide file tree
Showing 108 changed files with 631 additions and 472 deletions.
22 changes: 1 addition & 21 deletions docs/oidc.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,9 @@
# OIDC and delegated authentication

## Compatibility/OIDC-aware mode

[MSC2965: OIDC provider discovery](https://github.com/matrix-org/matrix-spec-proposals/pull/2965)
[MSC3824: OIDC aware clients](https://github.com/matrix-org/matrix-spec-proposals/pull/3824)
This mode uses an SSO flow to gain a `loginToken` from the authentication provider, then continues with SSO login.
Element Web uses [MSC2965: OIDC provider discovery](https://github.com/matrix-org/matrix-spec-proposals/pull/2965) to discover the configured provider.
Wherever valid MSC2965 configuration is discovered, OIDC-aware login flow will be the only option offered.

## (🧪Experimental) OIDC-native flow

Can be enabled by a config-level-only setting in `config.json`

```json
{
"features": {
"feature_oidc_native_flow": true
}
}
```

See https://areweoidcyet.com/client-implementation-guide/ for implementation details.

Element Web uses [MSC2965: OIDC provider discovery](https://github.com/matrix-org/matrix-spec-proposals/pull/2965) to discover the configured provider.
Where OIDC native login flow is enabled and valid MSC2965 configuration is discovered, OIDC native login flow will be the only login option offered.
Where a valid MSC2965 configuration is discovered, OIDC native login flow will be the only login option offered.
Element Web will attempt to [dynamically register](https://openid.net/specs/openid-connect-registration-1_0.html) with the configured OP.
Then, authentication will be completed [as described here](https://areweoidcyet.com/client-implementation-guide/).

Expand Down
2 changes: 2 additions & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ const config: Config = {
testEnvironment: "jsdom",
testEnvironmentOptions: {
url: "http://localhost/",
// This is needed to be able to load dual CJS/ESM WASM packages e.g. rust crypto & matrix-wywiwyg
customExportConditions: ["browser", "node"],
},
testMatch: ["<rootDir>/test/**/*-test.[tj]s?(x)"],
globalSetup: "<rootDir>/test/globalSetup.ts",
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
"@matrix-org/react-sdk-module-api": "^2.4.0",
"@matrix-org/spec": "^1.7.0",
"@sentry/browser": "^8.0.0",
"@types/png-chunks-extract": "^1.0.2",
"@vector-im/compound-design-tokens": "^2.0.1",
"@vector-im/compound-web": "^7.5.0",
"@vector-im/matrix-wysiwyg": "2.37.13",
Expand Down
34 changes: 0 additions & 34 deletions playwright/e2e/oidc/oidc-aware.spec.ts

This file was deleted.

4 changes: 0 additions & 4 deletions playwright/e2e/oidc/oidc-native.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,6 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => {
test.skip(isDendrite, "does not yet support MAS");
test.slow(); // trace recording takes a while here

test.use({
labsFlags: ["feature_oidc_native_flow"],
});

test("can register the oauth2 client and an account", async ({ context, page, homeserver, mailhog, mas }) => {
const tokenUri = `http://localhost:${mas.port}/oauth2/token`;
const tokenApiPromise = page.waitForRequest(

Check failure on line 19 in playwright/e2e/oidc/oidc-native.spec.ts

View workflow job for this annotation

GitHub Actions / Run Tests [Chrome] 3/6

[Chrome] › oidc/oidc-native.spec.ts:17:9 › OIDC Native › can register the oauth2 client and an account @no-firefox @no-webkit

1) [Chrome] › oidc/oidc-native.spec.ts:17:9 › OIDC Native › can register the oauth2 client and an account @no-firefox @no-webkit Error: page.waitForRequest: Test timeout of 90000ms exceeded. 17 | test("can register the oauth2 client and an account", async ({ context, page, homeserver, mailhog, mas }) => { 18 | const tokenUri = `http://localhost:${mas.port}/oauth2/token`; > 19 | const tokenApiPromise = page.waitForRequest( | ^ 20 | (request) => request.url() === tokenUri && request.postDataJSON()["grant_type"] === "authorization_code", 21 | ); 22 | at /home/runner/work/element-web/element-web/playwright/e2e/oidc/oidc-native.spec.ts:19:38
Expand Down
2 changes: 1 addition & 1 deletion playwright/plugins/homeserver/synapse/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { randB64Bytes } from "../../utils/rand";
// Docker tag to use for synapse docker image.
// We target a specific digest as every now and then a Synapse update will break our CI.
// This digest is updated by the playwright-image-updates.yaml workflow periodically.
const DOCKER_TAG = "develop@sha256:c965896a4865479ab2628807ebf6d9c742586f3b6185a56f10077a408f1c7c3b";
const DOCKER_TAG = "develop@sha256:cde72497d6d096445ce4eaf4b6797f871c67bc7ff7ea567a97e10676fb488796";

async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise<Omit<HomeserverConfig, "dockerUrl">> {
const templateDir = path.join(__dirname, "templates", opts.template);
Expand Down
8 changes: 5 additions & 3 deletions playwright/plugins/postgres/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@ export class PostgresDocker extends Docker {
super();
}

private async waitForPostgresReady(): Promise<void> {
private async waitForPostgresReady(ipAddress: string): Promise<void> {
const waitTimeMillis = 30000;
const startTime = new Date().getTime();
let lastErr: Error | null = null;
while (new Date().getTime() - startTime < waitTimeMillis) {
try {
await this.exec(["pg_isready", "-U", "postgres"], true);
// Note that we specify the IP address rather than letting it connect to the local
// socket: that's the listener we care about and empirically it matters.
await this.exec(["pg_isready", "-h", ipAddress, "-U", "postgres"], true);
lastErr = null;
break;
} catch (err) {
Expand Down Expand Up @@ -57,7 +59,7 @@ export class PostgresDocker extends Docker {
const ipAddress = await this.getContainerIp();
console.log(new Date(), "postgres container up");

await this.waitForPostgresReady();
await this.waitForPostgresReady(ipAddress);
console.log(new Date(), "postgres container ready");
return { ipAddress, containerId };
}
Expand Down
4 changes: 4 additions & 0 deletions scripts/analyse_unused_exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ ignore.push("/PinnedMessageBadge.tsx");
ignore.push("/editor/mock.ts");
ignore.push("DeviceIsolationModeController.ts");
ignore.push("urls.ts");
ignore.push("/json.ts");
ignore.push("/ReleaseAnnouncementStore.ts");
ignore.push("/WidgetLayoutStore.ts");
ignore.push("/common.ts");

// We ignore js-sdk by default as it may export for other non element-web projects
if (!includeJSSDK) ignore.push("matrix-js-sdk");
Expand Down
8 changes: 8 additions & 0 deletions src/@types/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,11 @@ type DeepReadonlyObject<T> = {
};

export type AtLeastOne<T, U = { [K in keyof T]: Pick<T, K> }> = Partial<T> & U[keyof U];

/**
* Returns a union type of the keys of the input Object type whose values are assignable to the given Item type.
* Based on https://stackoverflow.com/a/57862073
*/
export type Assignable<Object, Item> = {
[Key in keyof Object]: Object[Key] extends Item ? Key : never;
}[keyof Object];
13 changes: 13 additions & 0 deletions src/@types/json.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
Copyright 2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/

export type JsonValue = null | string | number | boolean;
export type JsonArray = Array<JsonValue | JsonObject | JsonArray>;
export interface JsonObject {
[key: string]: JsonObject | JsonArray | JsonValue;
}
export type Json = JsonArray | JsonObject;
18 changes: 0 additions & 18 deletions src/@types/png-chunks-extract.d.ts

This file was deleted.

15 changes: 0 additions & 15 deletions src/@types/sanitize-html.d.ts

This file was deleted.

7 changes: 3 additions & 4 deletions src/HtmlUtils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details.
*/

import React, { LegacyRef, ReactNode } from "react";
import sanitizeHtml from "sanitize-html";
import sanitizeHtml, { IOptions } from "sanitize-html";
import classNames from "classnames";
import katex from "katex";
import { decode } from "html-entities";
Expand All @@ -19,7 +19,6 @@ import { Optional } from "matrix-events-sdk";
import escapeHtml from "escape-html";
import { getEmojiFromUnicode } from "@matrix-org/emojibase-bindings";

import { IExtendedSanitizeOptions } from "./@types/sanitize-html";
import SettingsStore from "./settings/SettingsStore";
import { stripHTMLReply, stripPlainReply } from "./utils/Reply";
import { PERMITTED_URL_SCHEMES } from "./utils/UrlUtils";
Expand Down Expand Up @@ -126,7 +125,7 @@ export function isUrlPermitted(inputUrl: string): boolean {
}

// this is the same as the above except with less rewriting
const composerSanitizeHtmlParams: IExtendedSanitizeOptions = {
const composerSanitizeHtmlParams: IOptions = {
...sanitizeHtmlParams,
transformTags: {
"code": transformTags["code"],
Expand All @@ -135,7 +134,7 @@ const composerSanitizeHtmlParams: IExtendedSanitizeOptions = {
};

// reduced set of allowed tags to avoid turning topics into Myspace
const topicSanitizeHtmlParams: IExtendedSanitizeOptions = {
const topicSanitizeHtmlParams: IOptions = {
...sanitizeHtmlParams,
allowedTags: [
"font", // custom to matrix for IRC-style font coloring
Expand Down
7 changes: 3 additions & 4 deletions src/Linkify.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/

import React, { ReactElement } from "react";
import sanitizeHtml from "sanitize-html";
import sanitizeHtml, { IOptions } from "sanitize-html";
import { merge } from "lodash";
import _Linkify from "linkify-react";

Expand All @@ -17,7 +17,6 @@ import {
ELEMENT_URL_PATTERN,
options as linkifyMatrixOptions,
} from "./linkify-matrix";
import { IExtendedSanitizeOptions } from "./@types/sanitize-html";
import SettingsStore from "./settings/SettingsStore";
import { tryTransformPermalinkToLocalHref } from "./utils/permalinks/Permalinks";
import { mediaFromMxc } from "./customisations/Media";
Expand All @@ -26,7 +25,7 @@ import { PERMITTED_URL_SCHEMES } from "./utils/UrlUtils";
const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
const MEDIA_API_MXC_REGEX = /\/_matrix\/media\/r0\/(?:download|thumbnail)\/(.+?)\/(.+?)(?:[?/]|$)/;

export const transformTags: NonNullable<IExtendedSanitizeOptions["transformTags"]> = {
export const transformTags: NonNullable<IOptions["transformTags"]> = {
// custom to matrix
// add blank targets to all hyperlinks except vector URLs
"a": function (tagName: string, attribs: sanitizeHtml.Attributes) {
Expand Down Expand Up @@ -137,7 +136,7 @@ export const transformTags: NonNullable<IExtendedSanitizeOptions["transformTags"
},
};

export const sanitizeHtmlParams: IExtendedSanitizeOptions = {
export const sanitizeHtmlParams: IOptions = {
allowedTags: [
// These tags are suggested by the spec https://spec.matrix.org/v1.10/client-server-api/#mroommessage-msgtypes
"font", // custom to matrix for IRC-style font coloring
Expand Down
2 changes: 1 addition & 1 deletion src/Notifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ class NotifierClass extends TypedEventEmitter<keyof EmittedEvents, EmittedEvents
url: string;
name: string;
type: string;
size: string;
size: number;
} | null {
// We do no caching here because the SDK caches setting
// and the browser will cache the sound.
Expand Down
2 changes: 1 addition & 1 deletion src/components/structures/LoggedInView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ class LoggedInView extends React.Component<IProps, IState> {
} else {
backgroundImage = OwnProfileStore.instance.getHttpAvatarUrl();
}
this.setState({ backgroundImage });
this.setState({ backgroundImage: backgroundImage ?? undefined });
};

public canResetTimelineInRoom = (roomId: string): boolean => {
Expand Down
10 changes: 1 addition & 9 deletions src/components/structures/auth/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ import AuthHeader from "../../views/auth/AuthHeader";
import AccessibleButton, { ButtonEvent } from "../../views/elements/AccessibleButton";
import { ValidatedServerConfig } from "../../../utils/ValidatedServerConfig";
import { filterBoolean } from "../../../utils/arrays";
import { Features } from "../../../settings/Settings";
import { startOidcLogin } from "../../../utils/oidc/authorize";

interface IProps {
Expand Down Expand Up @@ -90,17 +89,13 @@ type OnPasswordLogin = {
*/
export default class LoginComponent extends React.PureComponent<IProps, IState> {
private unmounted = false;
private oidcNativeFlowEnabled = false;
private loginLogic!: Login;

private readonly stepRendererMap: Record<string, () => ReactNode>;

public constructor(props: IProps) {
super(props);

// only set on a config level, so we don't need to watch
this.oidcNativeFlowEnabled = SettingsStore.getValue(Features.OidcNativeFlow);

this.state = {
busy: false,
errorText: null,
Expand Down Expand Up @@ -358,10 +353,7 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>

const loginLogic = new Login(hsUrl, isUrl, fallbackHsUrl, {
defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
// if native OIDC is enabled in the client pass the server's delegated auth settings
delegatedAuthentication: this.oidcNativeFlowEnabled
? this.props.serverConfig.delegatedAuthentication
: undefined,
delegatedAuthentication: this.props.serverConfig.delegatedAuthentication,
});
this.loginLogic = loginLogic;

Expand Down
14 changes: 2 additions & 12 deletions src/components/structures/auth/Registration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ import { AuthHeaderDisplay } from "./header/AuthHeaderDisplay";
import { AuthHeaderProvider } from "./header/AuthHeaderProvider";
import SettingsStore from "../../../settings/SettingsStore";
import { ValidatedServerConfig } from "../../../utils/ValidatedServerConfig";
import { Features } from "../../../settings/Settings";
import { startOidcLogin } from "../../../utils/oidc/authorize";

const debuglog = (...args: any[]): void => {
Expand Down Expand Up @@ -130,8 +129,6 @@ export default class Registration extends React.Component<IProps, IState> {
private readonly loginLogic: Login;
// `replaceClient` tracks latest serverConfig to spot when it changes under the async method which fetches flows
private latestServerConfig?: ValidatedServerConfig;
// cache value from settings store
private oidcNativeFlowEnabled = false;

public constructor(props: IProps) {
super(props);
Expand All @@ -150,14 +147,10 @@ export default class Registration extends React.Component<IProps, IState> {
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;
this.loginLogic = new Login(hsUrl, isUrl, null, {
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: this.oidcNativeFlowEnabled ? delegatedAuthentication : undefined,
delegatedAuthentication,
});
}

Expand Down Expand Up @@ -227,10 +220,7 @@ export default class Registration extends React.Component<IProps, IState> {

this.loginLogic.setHomeserverUrl(hsUrl);
this.loginLogic.setIdentityServerUrl(isUrl);
// if native OIDC is enabled in the client pass the server's delegated auth settings
const delegatedAuthentication = this.oidcNativeFlowEnabled ? serverConfig.delegatedAuthentication : undefined;

this.loginLogic.setDelegatedAuthentication(delegatedAuthentication);
this.loginLogic.setDelegatedAuthentication(serverConfig.delegatedAuthentication);

let ssoFlow: SSOFlow | undefined;
let oidcNativeFlow: OidcNativeFlow | undefined;
Expand Down
3 changes: 2 additions & 1 deletion src/components/views/beta/BetaCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,13 @@ import SettingsFlag from "../elements/SettingsFlag";
import { useFeatureEnabled } from "../../../hooks/useSettings";
import InlineSpinner from "../elements/InlineSpinner";
import { shouldShowFeedback } from "../../../utils/Feedback";
import { FeatureSettingKey } from "../../../settings/Settings.tsx";

// XXX: Keep this around for re-use in future Betas

interface IProps {
title?: string;
featureId: string;
featureId: FeatureSettingKey;
}

interface IBetaPillProps {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ export const RoomGeneralContextMenu: React.FC<RoomGeneralContextMenuProps> = ({
}
})();

const developerModeEnabled = useSettingValue<boolean>("developerMode");
const developerModeEnabled = useSettingValue("developerMode");
const developerToolsOption = developerModeEnabled ? (
<DeveloperToolsOption onFinished={onFinished} roomId={room.roomId} />
) : null;
Expand Down
Loading

0 comments on commit 08373c0

Please sign in to comment.