OIDC: Check static client registration and add login flow (#11088)

* util functions to get static client id

* check static client ids in login flow

* remove dead code

* add trailing slash

* comment error enum

* spacing

* PR tidying

* more comments

* add ValidatedDelegatedAuthConfig type

* Update src/Login.ts

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Update src/Login.ts

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Update src/utils/ValidatedServerConfig.ts

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* rename oidc_static_clients to oidc_static_client_ids

* comment

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
This commit is contained in:
Kerry 2023-06-22 22:15:44 +12:00 committed by GitHub
parent 35f8c525aa
commit 328db8fdfd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 456 additions and 45 deletions

View file

@ -17,19 +17,29 @@ limitations under the License.
import React from "react";
import { fireEvent, render, screen, waitForElementToBeRemoved } from "@testing-library/react";
import { mocked, MockedObject } from "jest-mock";
import { createClient, MatrixClient } from "matrix-js-sdk/src/matrix";
import fetchMock from "fetch-mock-jest";
import { DELEGATED_OIDC_COMPATIBILITY, IdentityProviderBrand } from "matrix-js-sdk/src/@types/auth";
import { logger } from "matrix-js-sdk/src/logger";
import { createClient, MatrixClient } from "matrix-js-sdk/src/matrix";
import SdkConfig from "../../../../src/SdkConfig";
import { mkServerConfig, mockPlatformPeg, unmockPlatformPeg } from "../../../test-utils";
import Login from "../../../../src/components/structures/auth/Login";
import BasePlatform from "../../../../src/BasePlatform";
import SettingsStore from "../../../../src/settings/SettingsStore";
import { Features } from "../../../../src/settings/Settings";
import { ValidatedDelegatedAuthConfig } from "../../../../src/utils/ValidatedServerConfig";
import * as registerClientUtils from "../../../../src/utils/oidc/registerClient";
import { OidcClientError } from "../../../../src/utils/oidc/error";
jest.mock("matrix-js-sdk/src/matrix");
jest.useRealTimers();
const oidcStaticClientsConfig = {
"https://staticallyregisteredissuer.org/": "static-clientId-123",
};
describe("Login", function () {
let platform: MockedObject<BasePlatform>;
@ -42,6 +52,7 @@ describe("Login", function () {
SdkConfig.put({
brand: "test-brand",
disable_custom_urls: true,
oidc_static_client_ids: oidcStaticClientsConfig,
});
mockClient.login.mockClear().mockResolvedValue({});
mockClient.loginFlows.mockClear().mockResolvedValue({ flows: [{ type: "m.login.password" }] });
@ -51,6 +62,7 @@ describe("Login", function () {
return mockClient;
});
fetchMock.resetBehavior();
fetchMock.resetHistory();
fetchMock.get("https://matrix.org/_matrix/client/versions", {
unstable_features: {},
versions: [],
@ -66,10 +78,14 @@ describe("Login", function () {
unmockPlatformPeg();
});
function getRawComponent(hsUrl = "https://matrix.org", isUrl = "https://vector.im") {
function getRawComponent(
hsUrl = "https://matrix.org",
isUrl = "https://vector.im",
delegatedAuthentication?: ValidatedDelegatedAuthConfig,
) {
return (
<Login
serverConfig={mkServerConfig(hsUrl, isUrl)}
serverConfig={mkServerConfig(hsUrl, isUrl, delegatedAuthentication)}
onLoggedIn={() => {}}
onRegisterClick={() => {}}
onServerConfigChange={() => {}}
@ -77,8 +93,8 @@ describe("Login", function () {
);
}
function getComponent(hsUrl?: string, isUrl?: string) {
return render(getRawComponent(hsUrl, isUrl));
function getComponent(hsUrl?: string, isUrl?: string, delegatedAuthentication?: ValidatedDelegatedAuthConfig) {
return render(getRawComponent(hsUrl, isUrl, delegatedAuthentication));
}
it("should show form with change server link", async () => {
@ -190,6 +206,7 @@ describe("Login", function () {
versions: [],
});
rerender(getRawComponent("https://server2"));
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
fireEvent.click(container.querySelector(".mx_SSOButton")!);
expect(platform.startSingleSignOn.mock.calls[1][0].baseUrl).toBe("https://server2");
@ -319,4 +336,117 @@ describe("Login", function () {
// error cleared
expect(screen.queryByText("Your test-brand is misconfigured")).not.toBeInTheDocument();
});
describe("OIDC native flow", () => {
const hsUrl = "https://matrix.org";
const isUrl = "https://vector.im";
const issuer = "https://test.com/";
const delegatedAuth = {
issuer,
registrationEndpoint: issuer + "register",
tokenEndpoint: issuer + "token",
authorizationEndpoint: issuer + "authorization",
};
beforeEach(() => {
jest.spyOn(logger, "error");
jest.spyOn(SettingsStore, "getValue").mockImplementation(
(settingName) => settingName === Features.OidcNativeFlow,
);
});
afterEach(() => {
jest.spyOn(logger, "error").mockRestore();
});
it("should not attempt registration when oidc native flow setting is disabled", async () => {
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
getComponent(hsUrl, isUrl, delegatedAuth);
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
// continued with normal setup
expect(mockClient.loginFlows).toHaveBeenCalled();
// normal password login rendered
expect(screen.getByLabelText("Username")).toBeInTheDocument();
});
it("should attempt to register oidc client", async () => {
// dont mock, spy so we can check config values were correctly passed
jest.spyOn(registerClientUtils, "getOidcClientId");
getComponent(hsUrl, isUrl, delegatedAuth);
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
// called with values from config
expect(registerClientUtils.getOidcClientId).toHaveBeenCalledWith(
delegatedAuth,
"test-brand",
"http://localhost",
oidcStaticClientsConfig,
);
});
it("should fallback to normal login when client does not have static clientId", async () => {
getComponent(hsUrl, isUrl, delegatedAuth);
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
expect(logger.error).toHaveBeenCalledWith(new Error(OidcClientError.DynamicRegistrationNotSupported));
// continued with normal setup
expect(mockClient.loginFlows).toHaveBeenCalled();
// normal password login rendered
expect(screen.getByLabelText("Username")).toBeInTheDocument();
});
// short term during active development, UI will be added in next PRs
it("should show error when oidc native flow is correctly configured but not supported by UI", async () => {
const delegatedAuthWithStaticClientId = {
...delegatedAuth,
issuer: "https://staticallyregisteredissuer.org/",
};
getComponent(hsUrl, isUrl, delegatedAuthWithStaticClientId);
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
// did not continue with matrix login
expect(mockClient.loginFlows).not.toHaveBeenCalled();
// no oidc native UI yet
expect(
screen.getByText("This homeserver doesn't offer any login flows which are supported by this client."),
).toBeInTheDocument();
});
/**
* Oidc-aware flows still work while the oidc-native feature flag is disabled
*/
it("should show oidc-aware flow for oidc-enabled homeserver when oidc native flow setting is disabled", async () => {
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
mockClient.loginFlows.mockResolvedValue({
flows: [
{
type: "m.login.sso",
[DELEGATED_OIDC_COMPATIBILITY.name]: true,
},
{
type: "m.login.password",
},
],
});
const { container } = getComponent(hsUrl, isUrl, delegatedAuth);
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
// continued with normal setup
expect(mockClient.loginFlows).toHaveBeenCalled();
// oidc-aware 'continue' button displayed
const ssoButtons = container.querySelectorAll(".mx_SSOButton");
expect(ssoButtons.length).toBe(1);
expect(ssoButtons[0].textContent).toBe("Continue");
// no password form visible
expect(container.querySelector("form")).toBeFalsy();
});
});
});