OIDC: register (#11727)

* update uses of ValidatedDelegatedAuthConfig to broader OidcClientConfig type

* add OIDC register flow to registration page

* pass prompt param to auth url creation

* update type

* lint

* test registration oidc button

* fix: reference state inside setState

* comment
pull/28788/head^2
Kerry 2023-10-12 10:44:46 +13:00 committed by GitHub
parent a80cf58aa3
commit 5d169afb8a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 215 additions and 41 deletions

View File

@ -22,15 +22,16 @@ import {
DELEGATED_OIDC_COMPATIBILITY, DELEGATED_OIDC_COMPATIBILITY,
ILoginFlow, ILoginFlow,
LoginRequest, LoginRequest,
OidcClientConfig,
} from "matrix-js-sdk/src/matrix"; } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { IMatrixClientCreds } from "./MatrixClientPeg"; import { IMatrixClientCreds } from "./MatrixClientPeg";
import SecurityCustomisations from "./customisations/Security"; import SecurityCustomisations from "./customisations/Security";
import { ValidatedDelegatedAuthConfig } from "./utils/ValidatedServerConfig";
import { getOidcClientId } from "./utils/oidc/registerClient"; import { getOidcClientId } from "./utils/oidc/registerClient";
import { IConfigOptions } from "./IConfigOptions"; import { IConfigOptions } from "./IConfigOptions";
import SdkConfig from "./SdkConfig"; import SdkConfig from "./SdkConfig";
import { isUserRegistrationSupported } from "./utils/oidc/isUserRegistrationSupported";
/** /**
* Login flows supported by this client * Login flows supported by this client
@ -47,13 +48,13 @@ interface ILoginOptions {
* If this property is set, we will attempt an OIDC login using the delegated auth settings. * If this property is set, we will attempt an OIDC login using the delegated auth settings.
* The caller is responsible for checking that OIDC is enabled in the labs settings. * The caller is responsible for checking that OIDC is enabled in the labs settings.
*/ */
delegatedAuthentication?: ValidatedDelegatedAuthConfig; delegatedAuthentication?: OidcClientConfig;
} }
export default class Login { export default class Login {
private flows: Array<ClientLoginFlow> = []; private flows: Array<ClientLoginFlow> = [];
private readonly defaultDeviceDisplayName?: string; private readonly defaultDeviceDisplayName?: string;
private readonly delegatedAuthentication?: ValidatedDelegatedAuthConfig; private delegatedAuthentication?: OidcClientConfig;
private tempClient: MatrixClient | null = null; // memoize private tempClient: MatrixClient | null = null; // memoize
public constructor( public constructor(
@ -84,6 +85,15 @@ export default class Login {
this.isUrl = isUrl; this.isUrl = isUrl;
} }
/**
* Set delegated authentication config, clears tempClient.
* @param delegatedAuthentication delegated auth config, from ValidatedServerConfig
*/
public setDelegatedAuthentication(delegatedAuthentication?: OidcClientConfig): void {
this.tempClient = null; // clear memoization
this.delegatedAuthentication = delegatedAuthentication;
}
/** /**
* Get a temporary MatrixClient, which can be used for login or register * Get a temporary MatrixClient, which can be used for login or register
* requests. * requests.
@ -99,7 +109,12 @@ export default class Login {
return this.tempClient; return this.tempClient;
} }
public async getFlows(): Promise<Array<ClientLoginFlow>> { /**
* Get supported login flows
* @param isRegistration OPTIONAL used to verify registration is supported in delegated authentication config
* @returns Promise that resolves to supported login flows
*/
public async getFlows(isRegistration?: boolean): Promise<Array<ClientLoginFlow>> {
// try to use oidc native flow if we have delegated auth config // try to use oidc native flow if we have delegated auth config
if (this.delegatedAuthentication) { if (this.delegatedAuthentication) {
try { try {
@ -107,6 +122,7 @@ export default class Login {
this.delegatedAuthentication, this.delegatedAuthentication,
SdkConfig.get().brand, SdkConfig.get().brand,
SdkConfig.get().oidc_static_clients, SdkConfig.get().oidc_static_clients,
isRegistration,
); );
return [oidcFlow]; return [oidcFlow];
} catch (error) { } catch (error) {
@ -209,14 +225,20 @@ export interface OidcNativeFlow extends ILoginFlow {
* @param delegatedAuthConfig Auth config from ValidatedServerConfig * @param delegatedAuthConfig Auth config from ValidatedServerConfig
* @param clientName Client name to register with the OP, eg 'Element', used during client registration with OP * @param clientName Client name to register with the OP, eg 'Element', used during client registration with OP
* @param staticOidcClientIds static client config from config.json, used during client registration with OP * @param staticOidcClientIds static client config from config.json, used during client registration with OP
* @param isRegistration true when we are attempting registration
* @returns Promise<OidcNativeFlow> when oidc native authentication flow is supported and correctly configured * @returns Promise<OidcNativeFlow> when oidc native authentication flow is supported and correctly configured
* @throws when client can't register with OP, or any unexpected error * @throws when client can't register with OP, or any unexpected error
*/ */
const tryInitOidcNativeFlow = async ( const tryInitOidcNativeFlow = async (
delegatedAuthConfig: ValidatedDelegatedAuthConfig, delegatedAuthConfig: OidcClientConfig,
brand: string, brand: string,
oidcStaticClients?: IConfigOptions["oidc_static_clients"], oidcStaticClients?: IConfigOptions["oidc_static_clients"],
isRegistration?: boolean,
): Promise<OidcNativeFlow> => { ): Promise<OidcNativeFlow> => {
// if registration is not supported, bail before attempting to get the clientId
if (isRegistration && !isUserRegistrationSupported(delegatedAuthConfig)) {
throw new Error("Registration is not supported by OP");
}
const clientId = await getOidcClientId(delegatedAuthConfig, brand, window.location.origin, oidcStaticClients); const clientId = await getOidcClientId(delegatedAuthConfig, brand, window.location.origin, oidcStaticClients);
const flow = { const flow = {

View File

@ -38,7 +38,7 @@ import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils";
import * as Lifecycle from "../../../Lifecycle"; import * as Lifecycle from "../../../Lifecycle";
import { IMatrixClientCreds, MatrixClientPeg } from "../../../MatrixClientPeg"; import { IMatrixClientCreds, MatrixClientPeg } from "../../../MatrixClientPeg";
import AuthPage from "../../views/auth/AuthPage"; import AuthPage from "../../views/auth/AuthPage";
import Login from "../../../Login"; import Login, { OidcNativeFlow } from "../../../Login";
import dis from "../../../dispatcher/dispatcher"; import dis from "../../../dispatcher/dispatcher";
import SSOButtons from "../../views/elements/SSOButtons"; import SSOButtons from "../../views/elements/SSOButtons";
import ServerPicker from "../../views/elements/ServerPicker"; import ServerPicker from "../../views/elements/ServerPicker";
@ -52,6 +52,8 @@ 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";
const debuglog = (...args: any[]): void => { const debuglog = (...args: any[]): void => {
if (SettingsStore.getValue("debug_registration")) { if (SettingsStore.getValue("debug_registration")) {
@ -123,12 +125,17 @@ interface IState {
// the SSO flow definition, this is fetched from /login as that's the only // the SSO flow definition, this is fetched from /login as that's the only
// place it is exposed. // place it is exposed.
ssoFlow?: SSOFlow; ssoFlow?: SSOFlow;
// the OIDC native login flow, when supported and enabled
// if present, must be used for registration
oidcNativeFlow?: OidcNativeFlow;
} }
export default class Registration extends React.Component<IProps, IState> { 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);
@ -147,9 +154,14 @@ export default class Registration extends React.Component<IProps, IState> {
serverDeadError: "", serverDeadError: "",
}; };
const { hsUrl, isUrl } = this.props.serverConfig; // 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, { 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: this.oidcNativeFlowEnabled ? delegatedAuthentication : undefined,
}); });
} }
@ -219,22 +231,38 @@ 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
const delegatedAuthentication = this.oidcNativeFlowEnabled ? serverConfig.delegatedAuthentication : undefined;
this.loginLogic.setDelegatedAuthentication(delegatedAuthentication);
let ssoFlow: SSOFlow | undefined; let ssoFlow: SSOFlow | undefined;
let oidcNativeFlow: OidcNativeFlow | undefined;
try { try {
const loginFlows = await this.loginLogic.getFlows(); const loginFlows = await this.loginLogic.getFlows(true);
if (serverConfig !== this.latestServerConfig) return; // discard, serverConfig changed from under us if (serverConfig !== this.latestServerConfig) return; // discard, serverConfig changed from under us
ssoFlow = loginFlows.find((f) => f.type === "m.login.sso" || f.type === "m.login.cas") as SSOFlow; ssoFlow = loginFlows.find((f) => f.type === "m.login.sso" || f.type === "m.login.cas") as SSOFlow;
oidcNativeFlow = loginFlows.find((f) => f.type === "oidcNativeFlow") as OidcNativeFlow;
} catch (e) { } catch (e) {
if (serverConfig !== this.latestServerConfig) return; // discard, serverConfig changed from under us if (serverConfig !== this.latestServerConfig) return; // discard, serverConfig changed from under us
logger.error("Failed to get login flows to check for SSO support", e); logger.error("Failed to get login flows to check for SSO support", e);
} }
this.setState({ this.setState(({ flows }) => ({
matrixClient: cli, matrixClient: cli,
ssoFlow, ssoFlow,
oidcNativeFlow,
// if we are using oidc native we won't continue with flow discovery on HS
// so set an empty array to indicate flows are no longer loading
flows: oidcNativeFlow ? [] : flows,
busy: false, busy: false,
}); }));
// don't need to check with homeserver for login flows
// since we are going to use OIDC native flow
if (oidcNativeFlow) {
return;
}
try { try {
// We do the first registration request ourselves to discover whether we need to // We do the first registration request ourselves to discover whether we need to
@ -513,6 +541,24 @@ export default class Registration extends React.Component<IProps, IState> {
<Spinner /> <Spinner />
</div> </div>
); );
} else if (this.state.matrixClient && this.state.oidcNativeFlow) {
return (
<AccessibleButton
className="mx_Login_fullWidthButton"
kind="primary"
onClick={async () => {
await startOidcLogin(
this.props.serverConfig.delegatedAuthentication!,
this.state.oidcNativeFlow!.clientId,
this.props.serverConfig.hsUrl,
this.props.serverConfig.isUrl,
true /* isRegistration */,
);
}}
>
{_t("action|continue")}
</AccessibleButton>
);
} else if (this.state.matrixClient && this.state.flows.length) { } else if (this.state.matrixClient && this.state.flows.length) {
let ssoSection: JSX.Element | undefined; let ssoSection: JSX.Element | undefined;
if (this.state.ssoFlow) { if (this.state.ssoFlow) {

View File

@ -35,11 +35,14 @@ export const startOidcLogin = async (
clientId: string, clientId: string,
homeserverUrl: string, homeserverUrl: string,
identityServerUrl?: string, identityServerUrl?: string,
isRegistration?: boolean,
): Promise<void> => { ): Promise<void> => {
const redirectUri = window.location.origin; const redirectUri = window.location.origin;
const nonce = randomString(10); const nonce = randomString(10);
const prompt = isRegistration ? "create" : undefined;
const authorizationUrl = await generateOidcAuthorizationUrl({ const authorizationUrl = await generateOidcAuthorizationUrl({
metadata: delegatedAuthConfig.metadata, metadata: delegatedAuthConfig.metadata,
redirectUri, redirectUri,
@ -47,6 +50,7 @@ export const startOidcLogin = async (
homeserverUrl, homeserverUrl,
identityServerUrl, identityServerUrl,
nonce, nonce,
prompt,
}); });
window.location.href = authorizationUrl; window.location.href = authorizationUrl;

View File

@ -0,0 +1,30 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { 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 const isUserRegistrationSupported = (delegatedAuthConfig: OidcClientConfig): boolean => {
// The OidcMetadata type from oidc-client-ts does not include `prompt_values_supported`
// even though it is part of the OIDC spec, so cheat TS here to access it
const supportedPrompts = (delegatedAuthConfig.metadata as Record<string, unknown>)["prompt_values_supported"];
return Array.isArray(supportedPrompts) && supportedPrompts?.includes("create");
};

View File

@ -18,7 +18,7 @@ import React from "react";
import { fireEvent, render, screen, waitForElementToBeRemoved } from "@testing-library/react"; import { fireEvent, render, screen, waitForElementToBeRemoved } from "@testing-library/react";
import { mocked, MockedObject } from "jest-mock"; import { mocked, MockedObject } from "jest-mock";
import fetchMock from "fetch-mock-jest"; import fetchMock from "fetch-mock-jest";
import { DELEGATED_OIDC_COMPATIBILITY, IdentityProviderBrand } from "matrix-js-sdk/src/matrix"; import { DELEGATED_OIDC_COMPATIBILITY, IdentityProviderBrand, OidcClientConfig } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import * as Matrix from "matrix-js-sdk/src/matrix"; import * as Matrix from "matrix-js-sdk/src/matrix";
import { OidcError } from "matrix-js-sdk/src/oidc/error"; import { OidcError } from "matrix-js-sdk/src/oidc/error";
@ -29,8 +29,8 @@ 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 { Features } from "../../../../src/settings/Settings";
import { ValidatedDelegatedAuthConfig } from "../../../../src/utils/ValidatedServerConfig";
import * as registerClientUtils from "../../../../src/utils/oidc/registerClient"; import * as registerClientUtils from "../../../../src/utils/oidc/registerClient";
import { makeDelegatedAuthConfig } from "../../../test-utils/oidc";
jest.useRealTimers(); jest.useRealTimers();
@ -85,7 +85,7 @@ describe("Login", function () {
function getRawComponent( function getRawComponent(
hsUrl = "https://matrix.org", hsUrl = "https://matrix.org",
isUrl = "https://vector.im", isUrl = "https://vector.im",
delegatedAuthentication?: ValidatedDelegatedAuthConfig, delegatedAuthentication?: OidcClientConfig,
) { ) {
return ( return (
<Login <Login
@ -97,7 +97,7 @@ describe("Login", function () {
); );
} }
function getComponent(hsUrl?: string, isUrl?: string, delegatedAuthentication?: ValidatedDelegatedAuthConfig) { function getComponent(hsUrl?: string, isUrl?: string, delegatedAuthentication?: OidcClientConfig) {
return render(getRawComponent(hsUrl, isUrl, delegatedAuthentication)); return render(getRawComponent(hsUrl, isUrl, delegatedAuthentication));
} }
@ -377,12 +377,7 @@ describe("Login", function () {
const hsUrl = "https://matrix.org"; const hsUrl = "https://matrix.org";
const isUrl = "https://vector.im"; const isUrl = "https://vector.im";
const issuer = "https://test.com/"; const issuer = "https://test.com/";
const delegatedAuth = { const delegatedAuth = makeDelegatedAuthConfig(issuer);
issuer,
registrationEndpoint: issuer + "register",
tokenEndpoint: issuer + "token",
authorizationEndpoint: issuer + "authorization",
};
beforeEach(() => { beforeEach(() => {
jest.spyOn(logger, "error"); jest.spyOn(logger, "error");
jest.spyOn(SettingsStore, "getValue").mockImplementation( jest.spyOn(SettingsStore, "getValue").mockImplementation(
@ -412,7 +407,7 @@ describe("Login", function () {
it("should attempt to register oidc client", async () => { it("should attempt to register oidc client", async () => {
// dont mock, spy so we can check config values were correctly passed // dont mock, spy so we can check config values were correctly passed
jest.spyOn(registerClientUtils, "getOidcClientId"); jest.spyOn(registerClientUtils, "getOidcClientId");
fetchMock.post(delegatedAuth.registrationEndpoint, { status: 500 }); fetchMock.post(delegatedAuth.registrationEndpoint!, { status: 500 });
getComponent(hsUrl, isUrl, delegatedAuth); getComponent(hsUrl, isUrl, delegatedAuth);
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…")); await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
@ -429,7 +424,7 @@ describe("Login", function () {
}); });
it("should fallback to normal login when client registration fails", async () => { it("should fallback to normal login when client registration fails", async () => {
fetchMock.post(delegatedAuth.registrationEndpoint, { status: 500 }); fetchMock.post(delegatedAuth.registrationEndpoint!, { status: 500 });
getComponent(hsUrl, isUrl, delegatedAuth); getComponent(hsUrl, isUrl, delegatedAuth);
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…")); await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
@ -446,7 +441,7 @@ describe("Login", function () {
// short term during active development, UI will be added in next PRs // short term during active development, UI will be added in next PRs
it("should show continue button when oidc native flow is correctly configured", async () => { it("should show continue button when oidc native flow is correctly configured", async () => {
fetchMock.post(delegatedAuth.registrationEndpoint, { client_id: "abc123" }); fetchMock.post(delegatedAuth.registrationEndpoint!, { client_id: "abc123" });
getComponent(hsUrl, isUrl, delegatedAuth); getComponent(hsUrl, isUrl, delegatedAuth);
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…")); await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));

View File

@ -17,13 +17,21 @@ limitations under the License.
import React from "react"; import React from "react";
import { fireEvent, render, screen, waitForElementToBeRemoved } from "@testing-library/react"; import { fireEvent, render, screen, waitForElementToBeRemoved } from "@testing-library/react";
import { createClient, MatrixClient, MatrixError } from "matrix-js-sdk/src/matrix"; import { createClient, MatrixClient, MatrixError, OidcClientConfig } from "matrix-js-sdk/src/matrix";
import { mocked } from "jest-mock"; import { mocked, MockedObject } from "jest-mock";
import fetchMock from "fetch-mock-jest"; import fetchMock from "fetch-mock-jest";
import SdkConfig, { DEFAULTS } from "../../../../src/SdkConfig"; import SdkConfig, { DEFAULTS } from "../../../../src/SdkConfig";
import { mkServerConfig, mockPlatformPeg, unmockPlatformPeg } from "../../../test-utils"; import { getMockClientWithEventEmitter, mkServerConfig, mockPlatformPeg, unmockPlatformPeg } 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 SettingsStore from "../../../../src/settings/SettingsStore";
import { Features } from "../../../../src/settings/Settings";
import { startOidcLogin } from "../../../../src/utils/oidc/authorize";
jest.mock("../../../../src/utils/oidc/authorize", () => ({
startOidcLogin: jest.fn(),
}));
jest.mock("matrix-js-sdk/src/matrix", () => ({ jest.mock("matrix-js-sdk/src/matrix", () => ({
...jest.requireActual("matrix-js-sdk/src/matrix"), ...jest.requireActual("matrix-js-sdk/src/matrix"),
@ -32,18 +40,18 @@ jest.mock("matrix-js-sdk/src/matrix", () => ({
jest.useFakeTimers(); jest.useFakeTimers();
describe("Registration", function () { describe("Registration", function () {
const registerRequest = jest.fn(); let mockClient!: MockedObject<MatrixClient>;
const mockClient = mocked({
registerRequest,
loginFlows: jest.fn(),
getVersions: jest.fn().mockResolvedValue({ versions: ["v1.1"] }),
} as unknown as MatrixClient);
beforeEach(function () { beforeEach(function () {
SdkConfig.put({ SdkConfig.put({
...DEFAULTS, ...DEFAULTS,
disable_custom_urls: true, disable_custom_urls: true,
}); });
mockClient = getMockClientWithEventEmitter({
registerRequest: jest.fn(),
loginFlows: jest.fn(),
getVersions: jest.fn().mockResolvedValue({ versions: ["v1.1"] }),
});
mockClient.registerRequest.mockRejectedValueOnce( mockClient.registerRequest.mockRejectedValueOnce(
new MatrixError( new MatrixError(
{ {
@ -52,12 +60,13 @@ describe("Registration", function () {
401, 401,
), ),
); );
mockClient.loginFlows.mockClear().mockResolvedValue({ flows: [{ type: "m.login.password" }] }); mockClient.loginFlows.mockResolvedValue({ flows: [{ type: "m.login.password" }] });
mocked(createClient).mockImplementation((opts) => { mocked(createClient).mockImplementation((opts) => {
mockClient.idBaseUrl = opts.idBaseUrl; mockClient.idBaseUrl = opts.idBaseUrl;
mockClient.baseUrl = opts.baseUrl; mockClient.baseUrl = opts.baseUrl;
return mockClient; return mockClient;
}); });
fetchMock.catch(404);
fetchMock.get("https://matrix.org/_matrix/client/versions", { fetchMock.get("https://matrix.org/_matrix/client/versions", {
unstable_features: {}, unstable_features: {},
versions: ["v1.1"], versions: ["v1.1"],
@ -68,6 +77,7 @@ describe("Registration", function () {
}); });
afterEach(function () { afterEach(function () {
jest.restoreAllMocks();
fetchMock.restore(); fetchMock.restore();
SdkConfig.reset(); // we touch the config, so clean up SdkConfig.reset(); // we touch the config, so clean up
unmockPlatformPeg(); unmockPlatformPeg();
@ -80,12 +90,15 @@ describe("Registration", function () {
onServerConfigChange: jest.fn(), onServerConfigChange: jest.fn(),
}; };
function getRawComponent(hsUrl = "https://matrix.org", isUrl = "https://vector.im") { const defaultHsUrl = "https://matrix.org";
return <Registration {...defaultProps} serverConfig={mkServerConfig(hsUrl, isUrl)} />; const defaultIsUrl = "https://vector.im";
function getRawComponent(hsUrl = defaultHsUrl, isUrl = defaultIsUrl, authConfig?: OidcClientConfig) {
return <Registration {...defaultProps} serverConfig={mkServerConfig(hsUrl, isUrl, authConfig)} />;
} }
function getComponent(hsUrl?: string, isUrl?: string) { function getComponent(hsUrl?: string, isUrl?: string, authConfig?: OidcClientConfig) {
return render(getRawComponent(hsUrl, isUrl)); return render(getRawComponent(hsUrl, isUrl, authConfig));
} }
it("should show server picker", async function () { it("should show server picker", async function () {
@ -121,7 +134,7 @@ describe("Registration", function () {
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…")); await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
fireEvent.click(container.querySelector(".mx_SSOButton")!); fireEvent.click(container.querySelector(".mx_SSOButton")!);
expect(registerRequest.mock.instances[0].baseUrl).toBe("https://matrix.org"); expect(mockClient.baseUrl).toBe("https://matrix.org");
fetchMock.get("https://server2/_matrix/client/versions", { fetchMock.get("https://server2/_matrix/client/versions", {
unstable_features: {}, unstable_features: {},
@ -131,6 +144,69 @@ describe("Registration", function () {
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…")); await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
fireEvent.click(container.querySelector(".mx_SSOButton")!); fireEvent.click(container.querySelector(".mx_SSOButton")!);
expect(registerRequest.mock.instances[1].baseUrl).toBe("https://server2"); expect(mockClient.baseUrl).toBe("https://server2");
});
describe("when delegated authentication is configured and enabled", () => {
const authConfig = makeDelegatedAuthConfig();
const clientId = "test-client-id";
// @ts-ignore
authConfig.metadata["prompt_values_supported"] = ["create"];
beforeEach(() => {
// mock a statically registered client to avoid dynamic registration
SdkConfig.put({
oidc_static_clients: {
[authConfig.issuer]: {
client_id: clientId,
},
},
});
});
describe("when oidc native flow is not enabled in settings", () => {
beforeEach(() => {
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
});
it("should display user/pass registration form", async () => {
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", () => {
beforeEach(() => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((key) => key === Features.OidcNativeFlow);
});
it("should display oidc-native continue button", async () => {
const { container } = getComponent(defaultHsUrl, defaultIsUrl, authConfig);
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
// no form
expect(container.querySelector("form")).toBeFalsy();
expect(screen.getByText("Continue")).toBeTruthy();
});
it("should start OIDC login flow as registration on button click", async () => {
getComponent(defaultHsUrl, defaultIsUrl, authConfig);
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
fireEvent.click(screen.getByText("Continue"));
expect(startOidcLogin).toHaveBeenCalledWith(
authConfig,
clientId,
defaultHsUrl,
defaultIsUrl,
// isRegistration
true,
);
});
});
}); });
}); });

View File

@ -37,6 +37,7 @@ import {
RelationType, RelationType,
JoinRule, JoinRule,
IEventDecryptionResult, IEventDecryptionResult,
OidcClientConfig,
} from "matrix-js-sdk/src/matrix"; } from "matrix-js-sdk/src/matrix";
import { normalize } from "matrix-js-sdk/src/utils"; import { normalize } from "matrix-js-sdk/src/utils";
import { ReEmitter } from "matrix-js-sdk/src/ReEmitter"; import { ReEmitter } from "matrix-js-sdk/src/ReEmitter";
@ -47,7 +48,7 @@ import { MapperOpts } from "matrix-js-sdk/src/event-mapper";
import type { GroupCall } from "matrix-js-sdk/src/matrix"; import type { GroupCall } from "matrix-js-sdk/src/matrix";
import { MatrixClientPeg as peg } from "../../src/MatrixClientPeg"; import { MatrixClientPeg as peg } from "../../src/MatrixClientPeg";
import { ValidatedDelegatedAuthConfig, ValidatedServerConfig } from "../../src/utils/ValidatedServerConfig"; import { ValidatedServerConfig } from "../../src/utils/ValidatedServerConfig";
import { EnhancedMap } from "../../src/utils/maps"; import { EnhancedMap } from "../../src/utils/maps";
import { AsyncStoreWithClient } from "../../src/stores/AsyncStoreWithClient"; import { AsyncStoreWithClient } from "../../src/stores/AsyncStoreWithClient";
import MatrixClientBackedSettingsHandler from "../../src/settings/handlers/MatrixClientBackedSettingsHandler"; import MatrixClientBackedSettingsHandler from "../../src/settings/handlers/MatrixClientBackedSettingsHandler";
@ -653,7 +654,7 @@ export function mkStubRoom(
export function mkServerConfig( export function mkServerConfig(
hsUrl: string, hsUrl: string,
isUrl: string, isUrl: string,
delegatedAuthentication?: ValidatedDelegatedAuthConfig, delegatedAuthentication?: OidcClientConfig,
): ValidatedServerConfig { ): ValidatedServerConfig {
return { return {
hsUrl, hsUrl,