From e52083e29a27b527e499cdcfaf0e42c705042515 Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti Date: Fri, 29 Aug 2025 10:30:54 -0400 Subject: [PATCH 1/2] Start supporting OAuth 2.0 authentication API --- locales/en/app.json | 1 + src/App.tsx | 2 + src/Avatar.test.tsx | 1 + src/ClientContext.tsx | 44 ++++- src/RichError.tsx | 28 ++- src/auth/LoginPage.tsx | 162 +++++++++++------- src/auth/OidcRedirectPage.tsx | 104 +++++++++++ src/config/ConfigOptions.ts | 21 ++- src/settings/useSubmitRageshake.test.tsx | 1 + src/utils/oidc/authorize.ts | 96 +++++++++++ src/utils/oidc/callbackUrl.ts | 14 ++ src/utils/oidc/discovery.ts | 30 ++++ src/utils/oidc/error.ts | 16 ++ src/utils/oidc/isUserRegistrationSupported.ts | 21 +++ src/utils/oidc/registerClient.ts | 61 +++++++ 15 files changed, 526 insertions(+), 76 deletions(-) create mode 100644 src/auth/OidcRedirectPage.tsx create mode 100644 src/utils/oidc/authorize.ts create mode 100644 src/utils/oidc/callbackUrl.ts create mode 100644 src/utils/oidc/discovery.ts create mode 100644 src/utils/oidc/error.ts create mode 100644 src/utils/oidc/isUserRegistrationSupported.ts create mode 100644 src/utils/oidc/registerClient.ts diff --git a/locales/en/app.json b/locales/en/app.json index 007e372a0..5f4154abe 100644 --- a/locales/en/app.json +++ b/locales/en/app.json @@ -90,6 +90,7 @@ "e2ee_unsupported_description": "Your web browser does not support encrypted calls. Supported browsers include Chrome, Safari, and Firefox 117+.", "generic": "Something went wrong", "generic_description": "Submitting debug logs will help us track down the problem.", + "homeserver_misconfig": "Misconfigured homeserver", "insufficient_capacity": "Insufficient capacity", "insufficient_capacity_description": "The server has reached its maximum capacity and you cannot join the call at this time. Try again later, or contact your server admin if the problem persists.", "matrix_rtc_focus_missing": "The server is not configured to work with {{brand}}. Please contact your server admin (Domain: {{domain}}, Error Code: {{ errorCode }}).", diff --git a/src/App.tsx b/src/App.tsx index b87f587ca..ba810aefc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -20,6 +20,7 @@ import { logger } from "matrix-js-sdk/lib/logger"; import { HomePage } from "./home/HomePage"; import { LoginPage } from "./auth/LoginPage"; +import { OidcRedirectPage } from "./auth/OidcRedirectPage"; import { RegisterPage } from "./auth/RegisterPage"; import { RoomPage } from "./room/RoomPage"; import { ClientProvider } from "./ClientContext"; @@ -88,6 +89,7 @@ export const App: FC = ({ vm }) => { } /> } /> + } /> } /> } /> diff --git a/src/Avatar.test.tsx b/src/Avatar.test.tsx index a02963e03..809d99a95 100644 --- a/src/Avatar.test.tsx +++ b/src/Avatar.test.tsx @@ -21,6 +21,7 @@ const TestComponent: FC< use(ClientContext); export function useClient(): { client?: MatrixClient; setClient?: (client: MatrixClient, session: Session) => void; + oidcClientConfig: OidcClientConfig | null | undefined; } { let client; let setClient; + let oidcClientConfig; const clientState = useClientState(); if (clientState?.state === "valid") { client = clientState.authenticated?.client; setClient = clientState.setClient; + oidcClientConfig = clientState.oidcClientConfig; } - return { client, setClient }; + return { client, setClient, oidcClientConfig }; } // Plain representation of the `ClientContext` as a helper for old components that expected an object with multiple fields. @@ -140,6 +145,12 @@ interface Props { export const ClientProvider: FC = ({ children }) => { const navigate = useNavigate(); + // null = no OIDC config, undefined = loading + const [oidcClientConfig, setOidcClientConfig] = useState< + OidcClientConfig | null | undefined + >(undefined); + const [oidcErr, setOidcErr] = useState(undefined); + // null = signed out, undefined = loading const [initClientState, setInitClientState] = useState< InitResult | null | undefined @@ -153,6 +164,11 @@ export const ClientProvider: FC = ({ children }) => { if (initializing.current) return; initializing.current = true; + if (!widget) { + // TODO: spec says this may change over time & should be refreshed upon cache expiry + getAuthMetadata().then(setOidcClientConfig, setOidcErr); + } + loadClient() .then((initResult) => { setInitClientState(initResult); @@ -251,12 +267,18 @@ export const ClientProvider: FC = ({ children }) => { const [supportsReactions, setSupportsReactions] = useState(false); const [supportsThumbnails, setSupportsThumbnails] = useState(false); - const state: ClientState | undefined = useMemo(() => { - if (alreadyOpenedErr) { - return { state: "error", error: alreadyOpenedErr }; + const state = useMemo((): ClientState | undefined => { + const error = alreadyOpenedErr || oidcErr; + if (error) { + return { state: "error", error }; } - if (initClientState === undefined) return undefined; + if ( + initClientState === undefined || + oidcClientConfig === undefined + ) { + return undefined; + } const authenticated = initClientState === null @@ -270,6 +292,7 @@ export const ClientProvider: FC = ({ children }) => { return { state: "valid", + oidcClientConfig, authenticated, setClient, disconnected: isDisconnected, @@ -280,6 +303,8 @@ export const ClientProvider: FC = ({ children }) => { }; }, [ alreadyOpenedErr, + oidcErr, + oidcClientConfig, changePassword, initClientState, logout, @@ -345,8 +370,9 @@ export const ClientProvider: FC = ({ children }) => { }; }, [initClientState, onSync]); - if (alreadyOpenedErr) { - return ; + const error = alreadyOpenedErr || oidcErr; + if (error) { + return ; } return {children}; diff --git a/src/RichError.tsx b/src/RichError.tsx index 699486e25..805d5561d 100644 --- a/src/RichError.tsx +++ b/src/RichError.tsx @@ -6,7 +6,7 @@ Please see LICENSE in the repository root for full details. */ import { useTranslation } from "react-i18next"; -import { PopOutIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; +import { ErrorIcon, PopOutIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import type { FC, ReactNode } from "react"; import { ErrorView } from "./ErrorView"; @@ -51,3 +51,29 @@ export class OpenElsewhereError extends RichError { super("App opened in another tab", ); } } + +const HomeserverMisconfig: FC<{message?: string}> = (props) => { + const { t } = useTranslation(); + + // TODO: don't want to show "Return to home screen" button for an error as fatal as this + return ( + + {props.message && ( +

+ {props.message} +

+ )} +
+ ); +}; + +export class HomeserverMisconfigError extends RichError { + public constructor(message?: string) { + super(message || "Unknown error", ); + } +} diff --git a/src/auth/LoginPage.tsx b/src/auth/LoginPage.tsx index da20a86be..0b7111752 100644 --- a/src/auth/LoginPage.tsx +++ b/src/auth/LoginPage.tsx @@ -1,11 +1,11 @@ /* -Copyright 2021-2024 New Vector Ltd. +Copyright 2021-2025 New Vector Ltd. SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE in the repository root for full details. */ -import { type FC, type FormEvent, useCallback, useRef, useState } from "react"; +import { type FC, type FormEvent, type ReactElement, useCallback, useRef, useState } from "react"; import { useNavigate, useLocation } from "react-router-dom"; import { Trans, useTranslation } from "react-i18next"; import { Button } from "@vector-im/compound-web"; @@ -19,13 +19,14 @@ import { usePageTitle } from "../usePageTitle"; import { PosthogAnalytics } from "../analytics/PosthogAnalytics"; import { Config } from "../config/Config"; import { Link } from "../button/Link"; +import { LoadingPage } from "../FullScreenView"; +import { getOidcClientId } from "../utils/oidc/registerClient"; +import { startOidcLogin } from "../utils/oidc/authorize"; export const LoginPage: FC = () => { const { t } = useTranslation(); usePageTitle(t("login_title")); - const { client, setClient } = useClient(); - const login = useInteractiveLogin(client); const homeserver = Config.defaultHomeserverUrl(); // TODO: Make this configurable const usernameRef = useRef(null); const passwordRef = useRef(null); @@ -36,19 +37,23 @@ export const LoginPage: FC = () => { // TODO: Handle hitting login page with authenticated client - const onSubmitLoginForm = useCallback( - (e: FormEvent) => { - e.preventDefault(); - setLoading(true); + let node: ReactElement; + const { client, setClient, oidcClientConfig } = useClient(); + if (oidcClientConfig === null) { + const login = useInteractiveLogin(client); + const onSubmitLoginForm = useCallback( + async (e: FormEvent): Promise => { + e.preventDefault(); + setLoading(true); - if (!homeserver || !usernameRef.current || !passwordRef.current) { - setError(Error("Login parameters are undefined")); - setLoading(false); - return; - } + if (!homeserver) { + setError(Error("Login parameters are undefined")); + setLoading(false); + return; + } - login(homeserver, usernameRef.current.value, passwordRef.current.value) - .then(async ([client, session]) => { + try { + const [client, session] = await login(homeserver, usernameRef.current?.value ?? "", passwordRef.current?.value ?? ""); if (!setClient) { return; } @@ -66,20 +71,84 @@ export const LoginPage: FC = () => { await navigate("/"); } PosthogAnalytics.instance.eventLogin.track(); - }) - .catch((error) => { + } catch (error: any) { setError(error); setLoading(false); - }); - }, - [login, location, navigate, homeserver, setClient], - ); - // we need to limit the length of the homserver name to not cover the whole loginview input with the string. - let shortendHomeserverName = Config.defaultServerName()?.slice(0, 25); - shortendHomeserverName = - shortendHomeserverName?.length !== Config.defaultServerName()?.length - ? shortendHomeserverName + "..." - : shortendHomeserverName; + } + }, + [login, location, navigate, homeserver, setClient], + ); + // we need to limit the length of the homserver name to not cover the whole loginview input with the string. + let shortendHomeserverName = Config.defaultServerName()?.slice(0, 25); + shortendHomeserverName = + shortendHomeserverName?.length !== Config.defaultServerName()?.length + ? shortendHomeserverName + "..." + : shortendHomeserverName; + node = ( +
+ + + + + + + {error && ( + + + + )} + + + +
+ ); + } else if (oidcClientConfig !== undefined) { + if (!homeserver) { + setError(Error("No homeserver is configured")); + setLoading(false); + return; + } + + node = ; + } else { + return ; + } + return ( <>
@@ -89,44 +158,7 @@ export const LoginPage: FC = () => {

{t("log_in")}

{t("login_subheading")}

-
- - - - - - - {error && ( - - - - )} - - - -
+ {node}

{t("login_auth_links_prompt")}

diff --git a/src/auth/OidcRedirectPage.tsx b/src/auth/OidcRedirectPage.tsx new file mode 100644 index 000000000..206372e6f --- /dev/null +++ b/src/auth/OidcRedirectPage.tsx @@ -0,0 +1,104 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +import { useState, type FC } from "react"; +import { useLocation, useNavigate } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import { createClient, type MatrixClient } from "matrix-js-sdk/src/matrix"; + +import { useClient } from "../ClientContext"; +import { usePageTitle } from "../usePageTitle"; +import { completeOidcLogin } from "../utils/oidc/authorize"; +import { initClient } from "../utils/matrix"; + +/** + * TODO: Yoinked from element-web, so move to SDK if possible + * Gets information about the owner of a given access token. + * @returns Promise that resolves with whoami response + * @throws when whoami request fails + */ +async function getUserIdFromAccessToken( + accessToken: string, + homeserverUrl: string, +): Promise> { + try { + const client = createClient({ + baseUrl: homeserverUrl, + accessToken: accessToken, + }); + + return await client.whoami(); + } catch (error) { + throw new Error("Failed to retrieve userId using accessToken"); + } +} + +export const OidcRedirectPage: FC = async () => { + const { t } = useTranslation(); + // TODO: probably want a new page title + usePageTitle(t("login_title")); + + const navigate = useNavigate(); + const location = useLocation(); + const [_, setError] = useState(); + + const { setClient } = useClient(); + if (!setClient) { + return; + } + + // TODO: make reactive + try { + const queryParams = new URLSearchParams(location.search); + const { accessToken, refreshToken, homeserverUrl, idToken, clientId, issuer } = + await completeOidcLogin(queryParams); + + const { + user_id: userId, + device_id: deviceId, + } = await getUserIdFromAccessToken(accessToken, homeserverUrl); + + const session = { + user_id: userId, + access_token: accessToken, + device_id: deviceId!, // TODO: make sure this really is always defined + passwordlessUser: false, + }; + + console.debug(`TODO: use ${refreshToken}`); + const client = await initClient( + { + baseUrl: homeserverUrl, + accessToken, + userId, + deviceId, + }, + false, + ); + setClient(client, session); + console.debug(`TODO: use ${clientId}, ${issuer}, ${idToken}`); + // persistOidcAuthenticatedSettings(clientId, issuer, idToken); + + const locationState = location.state; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + if (locationState && locationState.from) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + await navigate(locationState.from); + } else { + await navigate("/"); + } + } catch (error: any) { + setError(error); + return; + } + + return ( + <> + ); +} \ No newline at end of file diff --git a/src/config/ConfigOptions.ts b/src/config/ConfigOptions.ts index 40b2342be..0e3a7c06d 100644 --- a/src/config/ConfigOptions.ts +++ b/src/config/ConfigOptions.ts @@ -1,5 +1,5 @@ /* -Copyright 2022-2024 New Vector Ltd. +Copyright 2022-2025 New Vector Ltd. SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE in the repository root for full details. @@ -151,6 +151,25 @@ export interface ConfigOptions { */ membership_event_expiry_ms?: number; }; + + /** + * Configuration for OIDC issuers where a static client_id has been issued for the app. + * Otherwise dynamic client registration is attempted. + */ + oidc_static_clients?: { + [issuer: string]: { client_id: string }; + }; + + oidc_metadata?: { + client_name?: string; + client_uri?: string; + redirect_uris?: string[]; + logo_uri?: string; + application_type?: string; + tos_uri?: string; + policy_uri?: string; + contacts?: string[]; + } } // Overrides members from ConfigOptions that are always provided by the diff --git a/src/settings/useSubmitRageshake.test.tsx b/src/settings/useSubmitRageshake.test.tsx index b278d4b1c..7a616cd61 100644 --- a/src/settings/useSubmitRageshake.test.tsx +++ b/src/settings/useSubmitRageshake.test.tsx @@ -75,6 +75,7 @@ function renderWithMockClient( { + const redirectUri = getOidcCallbackUrl().href; + + const nonce = secureRandomString(10); + + const prompt = isRegistration ? "create" : undefined; + + const authorizationUrl = await generateOidcAuthorizationUrl({ + metadata: delegatedAuthConfig, + redirectUri, + clientId, + homeserverUrl, + identityServerUrl: undefined, + nonce, + prompt, + }); + + window.location.href = authorizationUrl; +} + +// TODO: Mostly yoinked from element-web, so move to SDK if possible +type CompleteOidcLoginResponse = { + // url of the homeserver selected during login + homeserverUrl: string; + // accessToken gained from OIDC token issuer + accessToken: string; + // refreshToken gained from OIDC token issuer, when falsy token cannot be refreshed + refreshToken?: string; + // idToken gained from OIDC token issuer + idToken: string; + // this client's id as registered with the OIDC issuer + clientId: string; + // issuer used during authentication + issuer: string; + // claims of the given access token; used during token refresh to validate new tokens + idTokenClaims: IdTokenClaims; +}; + +/** + * TODO: Mostly yoinked from element-web, so move to SDK if possible + * + * Attempt to complete authorization code flow to get an access token + * @param queryParams the query-parameters extracted from the real query-string of the starting URI. + * @returns Promise that resolves with a CompleteOidcLoginResponse when login was successful + * @throws When we failed to get a valid access token + */ +export async function completeOidcLogin(queryParams: URLSearchParams): Promise { + const code = queryParams.get("code"); + const state = queryParams.get("state"); + if (!code || !state) { + throw new Error(OidcClientError.InvalidQueryParameters); + } + const { homeserverUrl, tokenResponse, idTokenClaims, oidcClientSettings } = + await completeAuthorizationCodeGrant(code, state); + + return { + homeserverUrl, + accessToken: tokenResponse.access_token, + refreshToken: tokenResponse.refresh_token, + idToken: tokenResponse.id_token, + clientId: oidcClientSettings.clientId, + issuer: oidcClientSettings.issuer, + idTokenClaims, + }; +} \ No newline at end of file diff --git a/src/utils/oidc/callbackUrl.ts b/src/utils/oidc/callbackUrl.ts new file mode 100644 index 000000000..c5e0186a3 --- /dev/null +++ b/src/utils/oidc/callbackUrl.ts @@ -0,0 +1,14 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +/** + * The URL to return to after a successful OIDC authentication + */ +export function getOidcCallbackUrl(): URL { + // TODO: save the path somewhere + return new URL("after_login", window.location.origin); +} \ No newline at end of file diff --git a/src/utils/oidc/discovery.ts b/src/utils/oidc/discovery.ts new file mode 100644 index 000000000..f5ed72cbb --- /dev/null +++ b/src/utils/oidc/discovery.ts @@ -0,0 +1,30 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +import { MatrixClient, MatrixError, OidcClientConfig } from "matrix-js-sdk"; + +import { Config } from "../../config/Config"; +import { HomeserverMisconfigError } from "../../RichError"; + +export async function getAuthMetadata(): Promise { + const baseUrl = Config.defaultHomeserverUrl(); // TODO: Make this configurable + if (!baseUrl) { + throw new Error("No homeserver URL configured"); + } + + const tempClient = new MatrixClient({ baseUrl }); + try { + return await tempClient.getAuthMetadata(); + } catch (e) { + if (e instanceof MatrixError && e.httpStatus === 404 && e.errcode === "M_UNRECOGNIZED") { + // 404 M_UNRECOGNIZED means the server does not support OIDC + return null; + } else { + throw new HomeserverMisconfigError(e instanceof Error ? e.message : undefined); + } + } +} \ No newline at end of file diff --git a/src/utils/oidc/error.ts b/src/utils/oidc/error.ts new file mode 100644 index 000000000..9257de35f --- /dev/null +++ b/src/utils/oidc/error.ts @@ -0,0 +1,16 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +/** + * TODO: Yoinked from element-web, so move to SDK if possible + * + * Errors thrown by EC during OIDC native flow authentication. + * Intended to be logged, not read by users. + */ +export enum OidcClientError { + InvalidQueryParameters = "Invalid query parameters for OIDC native login. `code` and `state` are required.", +} \ No newline at end of file diff --git a/src/utils/oidc/isUserRegistrationSupported.ts b/src/utils/oidc/isUserRegistrationSupported.ts new file mode 100644 index 000000000..3342cb8be --- /dev/null +++ b/src/utils/oidc/isUserRegistrationSupported.ts @@ -0,0 +1,21 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +// TODO: File yoinked from element-web, so move to SDK if possible + +import { type OidcClientConfig } from "matrix-js-sdk/src/matrix"; + +/** + * Check the create prompt is supported by the OP, if so, we can do a registration flow + * https://openid.net/specs/openid-connect-prompt-create-1_0.html + * @param delegatedAuthConfig config as returned from discovery + * @returns whether user registration is supported + */ +export function isUserRegistrationSupported(delegatedAuthConfig: OidcClientConfig): boolean { + const supportedPrompts = delegatedAuthConfig.prompt_values_supported; + return Array.isArray(supportedPrompts) && supportedPrompts?.includes("create"); +} diff --git a/src/utils/oidc/registerClient.ts b/src/utils/oidc/registerClient.ts new file mode 100644 index 000000000..1a1d5e9ef --- /dev/null +++ b/src/utils/oidc/registerClient.ts @@ -0,0 +1,61 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +import { logger } from "matrix-js-sdk/src/logger"; +import { registerOidcClient, type OidcClientConfig } from "matrix-js-sdk"; + +import { ConfigOptions } from "../../config/ConfigOptions"; +import { Config } from "../../config/Config"; +import { getOidcCallbackUrl } from "./callbackUrl"; + +/** + * TODO: Mostly yoinked from element-web, so move to SDK if possible + * + * Get the statically configured clientId for the issuer + * @param issuer delegated auth OIDC issuer + * @param staticOidcClients static client config from config.json + * @returns clientId if found, otherwise undefined + */ +function getStaticOidcClientId( + issuer: string, + staticOidcClients?: ConfigOptions["oidc_static_clients"], +): string | undefined { + // static_oidc_clients are configured with a trailing slash + const issuerWithTrailingSlash = issuer.endsWith("/") ? issuer : issuer + "/"; + return staticOidcClients?.[issuerWithTrailingSlash]?.client_id; +} + +/** + * TODO: Mostly yoinked from element-web, so move to SDK if possible + * + * Get the statically configured clientId for an OIDC OP + * @param delegatedAuthConfig Auth config from OP + * @returns resolves with clientId + * @throws if no clientId is found + */ +export async function getOidcClientId( + delegatedAuthConfig: OidcClientConfig, +): Promise { + const config = Config.get(); + const staticClientId = getStaticOidcClientId(delegatedAuthConfig.issuer, config.oidc_static_clients); + if (staticClientId) { + logger.debug(`Using static clientId for issuer ${delegatedAuthConfig.issuer}`); + return staticClientId; + } + return await registerOidcClient( + delegatedAuthConfig, + { + clientName: config.oidc_metadata?.client_name ?? "Element Call", + clientUri: config.oidc_metadata?.client_uri ?? window.location.origin, + redirectUris: [getOidcCallbackUrl().href], + applicationType: "web", + contacts: config.oidc_metadata?.contacts, + tosUri: config.oidc_metadata?.tos_uri, + policyUri: config.oidc_metadata?.policy_uri, + }, + ); +} \ No newline at end of file From 9e89c4ced3d4902a9a1bd719bbdf4adb23eff920 Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti Date: Wed, 3 Sep 2025 15:36:42 -0400 Subject: [PATCH 2/2] Add example docker compose; fix dynamic reg --- README.md | 2 + backend/dev_homeserver.yaml | 9 +-- backend/dev_mas.yaml | 96 ++++++++++++++++++++++++++++++++ backend/dev_nginx.conf | 17 ++++++ config/config.devenv.json | 3 + dev-backend-docker-compose.yml | 27 +++++++++ src/App.tsx | 2 +- src/config/ConfigOptions.ts | 2 - src/utils/oidc/callbackUrl.ts | 2 +- src/utils/oidc/registerClient.ts | 1 + 10 files changed, 153 insertions(+), 8 deletions(-) create mode 100644 backend/dev_mas.yaml diff --git a/README.md b/README.md index 8ca7fa965..e0789fc0d 100644 --- a/README.md +++ b/README.md @@ -209,6 +209,7 @@ A docker compose file `dev-backend-docker-compose.yml` is provided to start the whole stack of components which is required for a local development environment: - Minimum Synapse Setup (servername: `synapse.m.localhost`) +- Matrix Authentication Service Setup (issuer: `mas.m.localhost`) - MatrixRTC Authorization Service (Note requires Federation API and hence a TLS reverse proxy) - Minimum LiveKit SFU Setup using dev defaults for config - Redis db for completeness @@ -218,6 +219,7 @@ whole stack of components which is required for a local development environment: certificates - Minimum TLS reverse proxy for - Synapse homeserver: `synapse.m.localhost` + - Matrix Authentication Service: `mas.m.localhost` - MatrixRTC backend: `matrix-rtc.m.localhost` - Local Element Call development `call.m.localhost` via `yarn dev --host ` - Element Web `app.m.localhost` diff --git a/backend/dev_homeserver.yaml b/backend/dev_homeserver.yaml index eab4e6986..6425dbf4a 100644 --- a/backend/dev_homeserver.yaml +++ b/backend/dev_homeserver.yaml @@ -12,6 +12,11 @@ listeners: - names: [client, federation, openid] compress: false +matrix_authentication_service: + enabled: true + secret: "mas-matrix-secret" + endpoint: http://mas:8080 + database: name: sqlite3 args: @@ -46,9 +51,5 @@ rc_message: per_second: 0.5 burst_count: 30 -# Required for Element Call in Single Page Mode due to on-the-fly user registration -enable_registration: true -enable_registration_without_verification: true - report_stats: false serve_server_wellknown: true diff --git a/backend/dev_mas.yaml b/backend/dev_mas.yaml new file mode 100644 index 000000000..54a87625b --- /dev/null +++ b/backend/dev_mas.yaml @@ -0,0 +1,96 @@ +http: + listeners: + - name: web + resources: + - name: discovery + - name: human + - name: oauth + - name: assets + - name: adminapi + binds: + - address: '[::]:8080' + proxy_protocol: false + trusted_proxies: + - 192.168.0.0/16 + - 172.16.0.0/12 + - 10.0.0.0/10 + - 127.0.0.1/8 + - fd00::/8 + - ::1/128 + public_base: https://mas.m.localhost/ +database: + uri: postgres://postgres@mas-db/mas + max_connections: 10 + min_connections: 0 + connect_timeout: 30 + idle_timeout: 600 + max_lifetime: 1800 +email: + from: '"Authentication Service" ' + reply_to: '"Authentication Service" ' + transport: blackhole +secrets: + encryption: 91c9eda308d874d1b8ba51c0fe3b7cbb868638c8fbb82d7eec0e6912586bdabd + keys: + - kid: H30QE7M5eX + key: | + -----BEGIN RSA PRIVATE KEY----- + MIIEogIBAAKCAQEA3+t9XjM8LhYMpagIKXnpTXVWxIo5zwM/R8wYRPg0MPGCrOUB + i/L/Vof4yK7lIMWtCT724e989PLR4YmG2pXBpB8P7SZ3feggLoUMi8+QNzyKsGD+ + lYmZd0D/2aVmxBxK83JP3LLodmoHduva2qSBF9YdZ6Greg/IcEdG2UY3iGYeojsQ + 1Wx7V2+WPeUCtaIONBPk/rwOgWmzAhqGyCXONbvGazElNEuM0fwI278qveP5kNoh + aL6HvlaYQbMSGAg1tf06AKOjsJG0CRsvsMdFxuG9GEwd4pJr9+v+OqwnaFJHJ70z + kQFC3s/w+xCk4NnO+jSopBptu8ycwjZYMuq2kQIDAQABAoIBAHbDqFL2Sc0H1N1o + KiwVhTCYM9U6mz65Mi8aiSTLoKL09aJONGvODrAOnl2SpeSj9AsbYkajh1tEDx3Y + m7YECBjMgN3/sREOtUL3PphJFuy1J7o1N9KIkOU3jHwbxk3t07MbxlAAdFuaESt6 + HTIqXm4OGrqEfTbYeC9VHrbPD1VAFj/OGHsYDurJzhfIlFSlZWZqHNjdNh2HAOJM + FElqJWqqR9fj2pYYdpo+oaheI/iIAuWpAgcZOJzaZ4iui4R9i4od+qqQ3EVECPvS + /QnezvDpiobShG6WOmrRj3WBtheiPLdNlNB0sVW9h3dHcrkE/l2n5pfArVbHB+wg + 4e5FEAECgYEA4XjB6hpN1iX66ADf27L5mHymu1hTojZLoQdy76OcjgG+4ZoQl71U + OAww4ek9I6Alz+aQqTAnRtLHBKH+xSuO2VoxQfimk71mUmByCr25vIU8mGtXRIJO + rtWEVE4HQPhK2LODLm5zbp3I1GsrfNgMCsuA/yse5MIczC8bRFyf+DECgYEA/jzx + ddl1asjArcFcQyKFJxdobNqYJ4P+rbDLIOmC9IQ8n/v+ETERCzj/y93yaXXqCBlR + uHDzo72F/+SYDDWGYanpRmN2cUv1A0XTUs+dWYjfrscFJUEx8CZh0GeoLE4H3uru + GlwqPnc9sMPee98mj4yDMyrNqLx/VaXV+wnpbGECgYAh4JoKSa9+SLCdYVxBT2/v + OHN43LmcOto8NLlRRl0EfUCn9xUdJ4Za8YH6v6e/DZYA2dzMfv63xn2+tXRpPbU1 + 9TZHekvVEPUp1XHtKTqaF87V+/LdyVJ3NH+whxTR7zyXuMkyFchkS3Lcb8nV9URB + 7vfP3zPCHWRkTYOkTuJ+UQKBgFCrj8ZgMOSoPJMlppvayTtFLypTFjJ7rIT6cwnH + bnkduIrfD5fu5MSV2nyauT+DXbYiKo8GsBhFm849f41oMnKs0ks2Zi++9UiLkGlX + XUs6phc0KUrP7AOSejkBmxgrzk2KZ/DPS8w0U8vR6reNcBPedwb2Tvl6jkDj9QjJ + 9VohAoGAISjlufqw3y8F/0on1AhqyROlJghTBsQ+xDEBZ9txx/HcKVghqMhBCbFj + LRf8B4vH9QXtlZVtPFj0wE3INxsYtilsbD8wbwkxsLeGUFdPDucPacfBSyX+wLRh + S1/twrPS9KVhkU5d0TbyfOlEB1OSXZTWZ1n9NaqOZUPf6FAwm78= + -----END RSA PRIVATE KEY----- + - kid: VrRd3Y2OeF + key: | + -----BEGIN EC PRIVATE KEY----- + MHcCAQEEIB3zBxhuh275A3piMMDZ2BEM6vsoxswNLrTJRiaY+m80oAoGCCqGSM49 + AwEHoUQDQgAECqFotDpyEYNrWf2UZaUB0CZz6KiptQL2wi8oRNkKlarjgDDNCBzR + dgCokx9C8bLpfqhTJE/6aSe6T19qkaPHIg== + -----END EC PRIVATE KEY----- + - kid: SHaCwxflXU + key: | + -----BEGIN EC PRIVATE KEY----- + MIGkAgEBBDCdchyP7aFxQce7vA+QMPkMkOaYKbmNoN1fnlKviKsJK1riq+1eKSEe + UeUF5BOczfugBwYFK4EEACKhZANiAAR1pIE4xN9xkULiCgMd/uztt4Lnu8FhvEZD + 3BhUfy5kdBVbYyk1khgKy3k+dQvXaTVkzsHkQN8K78WxlUDlF5zKXLjgkeEiqgz7 + HU0rr2e8geUiaEE2AkzWhvmIikvhuMo= + -----END EC PRIVATE KEY----- + - kid: ngjUaMfCuT + key: | + -----BEGIN EC PRIVATE KEY----- + MHQCAQEEIE11jnxjUvPk93ylMuIcwcayJsFUhsSH2EqAn97CiHf8oAcGBSuBBAAK + oUQDQgAE1XySwFNBUkzZ946MBf2/3ecXVptrauZEQ8d8zqUdBS7wOe5pZwZ15Jx4 + aZhlusZ3BPl0KiTlWwOlaRDMrw9EGA== + -----END EC PRIVATE KEY----- +passwords: + enabled: true + schemes: + - version: 1 + algorithm: argon2id + minimum_complexity: 0 +matrix: + kind: synapse + homeserver: synapse.m.localhost + secret: "mas-matrix-secret" + endpoint: http://homeserver:8008/ diff --git a/backend/dev_nginx.conf b/backend/dev_nginx.conf index a29b06d78..817363bb7 100644 --- a/backend/dev_nginx.conf +++ b/backend/dev_nginx.conf @@ -40,6 +40,23 @@ server { } +# Matrix Authentication Server reverse proxy +server { + listen 443 ssl; + listen [::]:443 ssl; + server_name mas.m.localhost; + ssl_certificate /root/ssl/cert.pem; + ssl_certificate_key /root/ssl/key.pem; + + location / { + proxy_pass "http://mas:8080"; + proxy_http_version 1.1; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + + error_page 500 502 503 504 /50x.html; +} + # MatrixRTC reverse proxy # - MatrixRTC Authorization Service # - LiveKit SFU websocket signaling connection diff --git a/config/config.devenv.json b/config/config.devenv.json index df0ff4c18..6f9470427 100644 --- a/config/config.devenv.json +++ b/config/config.devenv.json @@ -15,5 +15,8 @@ "delayed_leave_event_delay_ms": 18000, "delayed_leave_event_restart_ms": 4000, "network_error_retry_ms": 100 + }, + "oidc_metadata": { + "client_name": "Element Call (dev)" } } diff --git a/dev-backend-docker-compose.yml b/dev-backend-docker-compose.yml index e6180710b..458560674 100644 --- a/dev-backend-docker-compose.yml +++ b/dev-backend-docker-compose.yml @@ -68,6 +68,31 @@ services: volumes: - ./backend/synapse_tmp:/data:Z - ./backend/dev_homeserver.yaml:/data/cfg/homeserver.yaml:Z + depends_on: + - mas + networks: + - ecbackend + + mas: + # To add users, see `docker exec element-call-mas-1 mas-cli manage register-user -h` + image: ghcr.io/element-hq/matrix-authentication-service:latest + pull_policy: always + hostname: mas + volumes: + - ./backend/dev_mas.yaml:/config.yaml:ro,Z + depends_on: + - mas-db + networks: + - ecbackend + + mas-db: + image: docker.io/postgres:16-alpine + hostname: mas-db + restart: always + shm_size: 128mb + environment: + - POSTGRES_HOST_AUTH_METHOD=trust + - POSTGRES_DB=mas networks: - ecbackend @@ -101,7 +126,9 @@ services: - "host.docker.internal:host-gateway" depends_on: - synapse + - mas networks: ecbackend: aliases: + - mas.m.localhost - matrix-rtc.m.localhost diff --git a/src/App.tsx b/src/App.tsx index ba810aefc..2fda77adb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -89,7 +89,7 @@ export const App: FC = ({ vm }) => { } /> } /> - } /> + } /> } /> } /> diff --git a/src/config/ConfigOptions.ts b/src/config/ConfigOptions.ts index 0e3a7c06d..cfe608d95 100644 --- a/src/config/ConfigOptions.ts +++ b/src/config/ConfigOptions.ts @@ -163,9 +163,7 @@ export interface ConfigOptions { oidc_metadata?: { client_name?: string; client_uri?: string; - redirect_uris?: string[]; logo_uri?: string; - application_type?: string; tos_uri?: string; policy_uri?: string; contacts?: string[]; diff --git a/src/utils/oidc/callbackUrl.ts b/src/utils/oidc/callbackUrl.ts index c5e0186a3..85528515a 100644 --- a/src/utils/oidc/callbackUrl.ts +++ b/src/utils/oidc/callbackUrl.ts @@ -10,5 +10,5 @@ Please see LICENSE in the repository root for full details. */ export function getOidcCallbackUrl(): URL { // TODO: save the path somewhere - return new URL("after_login", window.location.origin); + return new URL("after-login", window.location.origin); } \ No newline at end of file diff --git a/src/utils/oidc/registerClient.ts b/src/utils/oidc/registerClient.ts index 1a1d5e9ef..02675dbc8 100644 --- a/src/utils/oidc/registerClient.ts +++ b/src/utils/oidc/registerClient.ts @@ -51,6 +51,7 @@ export async function getOidcClientId( { clientName: config.oidc_metadata?.client_name ?? "Element Call", clientUri: config.oidc_metadata?.client_uri ?? window.location.origin, + logoUri: config.oidc_metadata?.logo_uri, redirectUris: [getOidcCallbackUrl().href], applicationType: "web", contacts: config.oidc_metadata?.contacts,