Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Update MSC2965 OIDC Discovery implementation #12245

Merged
merged 39 commits into from
Feb 23, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
5a1ce08
Update MSC2965 OIDC Discovery implementation
t3chguy Feb 13, 2024
5099410
Remove unused interface
t3chguy Feb 13, 2024
ee26f17
Remove spurious test setup
t3chguy Feb 13, 2024
def3b9d
Remove spurious `--rm`
t3chguy Feb 14, 2024
14fca80
Fix OIDC-aware throwing error before resolving account management url
t3chguy Feb 14, 2024
d69a917
Split playwright postgres class out into ABC
t3chguy Feb 14, 2024
6b7d53b
Tidy up playwright fixtures
t3chguy Feb 14, 2024
9593e70
Add oidc-aware playwright test
t3chguy Feb 14, 2024
f2ddc5c
Avoid race condition where delegated account management is not shown …
t3chguy Feb 14, 2024
23bad14
delint
t3chguy Feb 14, 2024
b49125d
Make the app fixture lazy to avoid clobbering network routing
t3chguy Feb 14, 2024
a37c7a4
Iterate
t3chguy Feb 14, 2024
c887fd6
Fix authenticatedIssuer
t3chguy Feb 14, 2024
bf29874
Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into…
t3chguy Feb 14, 2024
07cb4cb
Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into…
t3chguy Feb 20, 2024
acdc987
Resolve race condition between opening settings & well-known check in…
t3chguy Feb 16, 2024
4d01fff
Add OIDC-aware and OIDC-native tests using MAS
t3chguy Feb 16, 2024
7f641a4
Merge branch 't3chguy/e2e-oidc-aware' of github.com:matrix-org/matrix…
t3chguy Feb 21, 2024
7688e97
Fix bad merge
t3chguy Feb 21, 2024
c5b50f0
Iterate
t3chguy Feb 21, 2024
9ebe5ab
Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into…
t3chguy Feb 21, 2024
a1ab218
Gather validated OIDC config during autodiscovery
t3chguy Feb 21, 2024
2f1a39b
Fix tests
t3chguy Feb 21, 2024
60d9b8c
Simplify
t3chguy Feb 21, 2024
68f8ea6
Add tests
t3chguy Feb 21, 2024
563274e
Simplify test
t3chguy Feb 22, 2024
0c4b03c
Iterate
t3chguy Feb 22, 2024
cacf98d
Iterate
t3chguy Feb 22, 2024
78a240a
Fix tests
t3chguy Feb 22, 2024
e9f5760
Fix tests
t3chguy Feb 22, 2024
9bd4b41
Pass error back instead of throwing it
t3chguy Feb 22, 2024
cffab76
Iterate
t3chguy Feb 22, 2024
212e883
Fix test
t3chguy Feb 22, 2024
10a2bc0
Fix tests
t3chguy Feb 22, 2024
9ead3eb
Merge branch 'develop' into t3chguy/fix/26908
t3chguy Feb 22, 2024
22f63b4
Iterate
t3chguy Feb 23, 2024
708a7d2
Merge remote-tracking branch 'origin/t3chguy/fix/26908' into t3chguy/…
t3chguy Feb 23, 2024
3dc8272
Apply suggestions from code review
t3chguy Feb 23, 2024
ef84256
Merge branch 'develop' into t3chguy/fix/26908
t3chguy Feb 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions src/stores/oidc/OidcClientStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import { MatrixClient } from "matrix-js-sdk/src/matrix";
import { discoverAndValidateOIDCIssuerWellKnown } from "matrix-js-sdk/src/oidc/discovery";
import { MatrixClient, discoverAndValidateOIDCIssuerWellKnown } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger";
import { OidcClient } from "oidc-client-ts";

Expand All @@ -25,6 +24,10 @@ import PlatformPeg from "../../PlatformPeg";
/**
* @experimental
* Stores information about configured OIDC provider
* In OIDC Native mode the client is registered with OIDC directly and maintains an OIDC token
* In OIDC Aware mode, the client is aware that the Server is using OIDC, but is using the standard Matrix APIs for most things,
* notable exceptions are account management where a link to the account management endpoint will be provided instead.
* Otherwise, the store is not operating, the auth is then in Legacy mode and everything uses normal Matrix APIs.
t3chguy marked this conversation as resolved.
Show resolved Hide resolved
*/
export class OidcClientStore {
private oidcClient?: OidcClient;
Expand All @@ -46,7 +49,7 @@ export class OidcClientStore {
if (this.authenticatedIssuer) {
await this.getOidcClient();
} else {
// Are we in OIDC-aware mode?
// We are not in OIDC Native mode as we have no locally stored issuer, check if the server delegates auth to OIDC.
t3chguy marked this conversation as resolved.
Show resolved Hide resolved
try {
const authIssuer = await this.matrixClient.getAuthIssuer();
const { accountManagementEndpoint, metadata } = await discoverAndValidateOIDCIssuerWellKnown(
Expand Down
10 changes: 8 additions & 2 deletions src/utils/AutoDiscoveryUtils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
discoverAndValidateOIDCIssuerWellKnown,
IClientWellKnown,
MatrixClient,
MatrixError,
OidcClientConfig,
} from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger";
Expand Down Expand Up @@ -297,12 +298,17 @@ export default class AutoDiscoveryUtils {
// This isn't inherently auto-discovery but used to be in an earlier incarnation of the MSC,
// and shuttling the data together makes a lot of sense
let delegatedAuthentication: OidcClientConfig | undefined;
let delegatedAuthenticationError: Error | undefined;
try {
const tempClient = new MatrixClient({ baseUrl: preferredHomeserverUrl });
const { issuer } = await tempClient.getAuthIssuer();
delegatedAuthentication = await discoverAndValidateOIDCIssuerWellKnown(issuer);
} catch (e) {
// This is expected for servers not delegating auth via OIDC
if (e instanceof MatrixError && e.httpStatus === 404 && e.errcode === "M_UNRECOGNIZED") {
// 404 M_UNRECOGNIZED means the server does not support OIDC
} else {
delegatedAuthenticationError = e as Error;
}
}

return {
Expand All @@ -311,7 +317,7 @@ export default class AutoDiscoveryUtils {
hsNameIsDifferent: url.hostname !== preferredHomeserverName,
isUrl: preferredIdentityUrl,
isDefault: false,
warning: hsResult.error,
warning: hsResult.error ?? delegatedAuthenticationError ?? null,
isNameResolvable: !isSynthetic,
delegatedAuthentication,
} as ValidatedServerConfig;
Expand Down
5 changes: 5 additions & 0 deletions test/components/structures/MatrixChat-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,11 @@ describe("<MatrixChat />", () => {
unstable_features: {},
versions: SERVER_SUPPORTED_MATRIX_VERSIONS,
});
fetchMock.catch({
status: 404,
body: '{"errcode": "M_UNRECOGNIZED", "error": "Unrecognized request"}',
headers: { "content-type": "application/json" },
});

jest.spyOn(StorageManager, "idbLoad").mockReset();
jest.spyOn(StorageManager, "idbSave").mockResolvedValue(undefined);
Expand Down
74 changes: 73 additions & 1 deletion test/components/structures/auth/Registration-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ import fetchMock from "fetch-mock-jest";
import SdkConfig, { DEFAULTS } from "../../../../src/SdkConfig";
import { getMockClientWithEventEmitter, mkServerConfig, mockPlatformPeg, unmockPlatformPeg } from "../../../test-utils";
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(),
Expand All @@ -33,7 +37,6 @@ jest.mock("matrix-js-sdk/src/matrix", () => ({
...jest.requireActual("matrix-js-sdk/src/matrix"),
createClient: jest.fn(),
}));
jest.useFakeTimers();

/** The matrix versions our mock server claims to support */
const SERVER_SUPPORTED_MATRIX_VERSIONS = ["v1.1", "v1.5", "v1.6", "v1.8", "v1.9"];
Expand Down Expand Up @@ -145,4 +148,73 @@ describe("Registration", function () {
fireEvent.click(container.querySelector(".mx_SSOButton")!);
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.metadata.issuer]: {
client_id: clientId,
},
},
});

fetchMock.get(`${defaultHsUrl}/_matrix/client/unstable/org.matrix.msc2965/auth_issuer`, {
issuer: authConfig.metadata.issuer,
});
fetchMock.get("https://auth.org/.well-known/openid-configuration", authConfig.metadata);
fetchMock.get(authConfig.metadata.jwks_uri!, { keys: [] });
});

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(await screen.findByText("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(await screen.findByText("Continue"));

expect(startOidcLogin).toHaveBeenCalledWith(
authConfig,
clientId,
defaultHsUrl,
defaultIsUrl,
// isRegistration
true,
);
});
});
});
});
5 changes: 5 additions & 0 deletions test/components/views/dialogs/ServerPickerDialog-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ describe("<ServerPickerDialog />", () => {
});

fetchMock.resetHistory();
fetchMock.catch({
status: 404,
body: '{"errcode": "M_UNRECOGNIZED", "error": "Unrecognized request"}',
headers: { "content-type": "application/json" },
});
});

it("should render dialog", () => {
Expand Down
8 changes: 5 additions & 3 deletions test/stores/oidc/OidcClientStore-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,15 @@ import fetchMock from "fetch-mock-jest";
import { mocked } from "jest-mock";
import { OidcClient } from "oidc-client-ts";
import { logger } from "matrix-js-sdk/src/logger";
import { discoverAndValidateOIDCIssuerWellKnown } from "matrix-js-sdk/src/oidc/discovery";
import { discoverAndValidateOIDCIssuerWellKnown } from "matrix-js-sdk/src/matrix";
import { OidcError } from "matrix-js-sdk/src/oidc/error";

import { OidcClientStore } from "../../../src/stores/oidc/OidcClientStore";
import { flushPromises, getMockClientWithEventEmitter, mockPlatformPeg } from "../../test-utils";
import { mockOpenIdConfiguration } from "../../test-utils/oidc";

jest.mock("matrix-js-sdk/src/oidc/discovery", () => ({
jest.mock("matrix-js-sdk/src/matrix", () => ({
...jest.requireActual("matrix-js-sdk/src/matrix"),
discoverAndValidateOIDCIssuerWellKnown: jest.fn(),
}));

Expand All @@ -52,6 +53,7 @@ describe("OidcClientStore", () => {
jest.spyOn(logger, "error").mockClear();

fetchMock.get(`${metadata.issuer}.well-known/openid-configuration`, metadata);
fetchMock.get(`${metadata.issuer}jwks`, { keys: [] });
mockPlatformPeg();
});

Expand Down Expand Up @@ -104,7 +106,7 @@ describe("OidcClientStore", () => {
mocked(discoverAndValidateOIDCIssuerWellKnown).mockRejectedValue(new Error(OidcError.OpSupport));
const store = new OidcClientStore(mockClient);

await flushPromises();
await store.readyPromise;

expect(logger.error).toHaveBeenCalledWith(
"Failed to initialise OidcClientStore",
Expand Down
1 change: 1 addition & 0 deletions test/test-utils/oidc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,5 @@ export const mockOpenIdConfiguration = (issuer = "https://auth.org/"): Validated
response_types_supported: ["code"],
grant_types_supported: ["authorization_code", "refresh_token"],
code_challenge_methods_supported: ["S256"],
account_management_uri: issuer + "account",
});
36 changes: 17 additions & 19 deletions test/utils/AutoDiscoveryUtils-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,17 @@ import { logger } from "matrix-js-sdk/src/logger";
import fetchMock from "fetch-mock-jest";

import AutoDiscoveryUtils from "../../src/utils/AutoDiscoveryUtils";
import { mockOpenIdConfiguration } from "../test-utils/oidc";

describe("AutoDiscoveryUtils", () => {
beforeEach(() => {
fetchMock.catch({
status: 404,
body: '{"errcode": "M_UNRECOGNIZED", "error": "Unrecognized request"}',
headers: { "content-type": "application/json" },
});
});

describe("buildValidatedConfigFromDiscovery()", () => {
const serverName = "my-server";

Expand Down Expand Up @@ -163,6 +172,7 @@ describe("AutoDiscoveryUtils", () => {
...expectedValidatedConfig,
hsNameIsDifferent: false,
hsName: "matrix.org",
warning: null,
});
});

Expand All @@ -181,6 +191,7 @@ describe("AutoDiscoveryUtils", () => {
...expectedValidatedConfig,
hsNameIsDifferent: true,
hsName: serverName,
warning: null,
});
});

Expand Down Expand Up @@ -228,20 +239,9 @@ describe("AutoDiscoveryUtils", () => {
},
);
fetchMock.get(`${issuer}.well-known/openid-configuration`, {
"issuer": `${issuer}`,
"authorization_endpoint": `${issuer}authorize`,
"token_endpoint": `${issuer}oauth2/token`,
"jwks_uri": `${issuer}oauth2/keys.json`,
"registration_endpoint": `${issuer}oauth2/registration`,
...mockOpenIdConfiguration(issuer),
"scopes_supported": ["openid", "email"],
"response_types_supported": ["code", "id_token", "code id_token"],
"response_modes_supported": ["form_post", "query", "fragment"],
"grant_types_supported": [
"authorization_code",
"refresh_token",
"client_credentials",
"urn:ietf:params:oauth:grant-type:device_code",
],
"token_endpoint_auth_methods_supported": [
"client_secret_basic",
"client_secret_post",
Expand All @@ -263,7 +263,6 @@ describe("AutoDiscoveryUtils", () => {
"ES384",
"ES256K",
],
"revocation_endpoint": `${issuer}oauth2/revoke`,
"revocation_endpoint_auth_methods_supported": [
"client_secret_basic",
"client_secret_post",
Expand Down Expand Up @@ -307,7 +306,6 @@ describe("AutoDiscoveryUtils", () => {
"ES384",
"ES256K",
],
"code_challenge_methods_supported": ["plain", "S256"],
"userinfo_endpoint": `${issuer}oauth2/userinfo`,
"subject_types_supported": ["public"],
"id_token_signing_alg_values_supported": [
Expand Down Expand Up @@ -350,7 +348,7 @@ describe("AutoDiscoveryUtils", () => {
"org.matrix.cross_signing_reset",
],
});
t3chguy marked this conversation as resolved.
Show resolved Hide resolved
fetchMock.get(`${issuer}oauth2/keys.json`, {
fetchMock.get(`${issuer}jwks`, {
keys: [],
});

Expand All @@ -373,15 +371,15 @@ describe("AutoDiscoveryUtils", () => {
"org.matrix.cross_signing_reset",
],
accountManagementEndpoint: "https://auth.matrix.org/account/",
authorizationEndpoint: "https://auth.matrix.org/authorize",
authorizationEndpoint: "https://auth.matrix.org/auth",
metadata: expect.objectContaining({
issuer,
}),
registrationEndpoint: "https://auth.matrix.org/oauth2/registration",
registrationEndpoint: "https://auth.matrix.org/registration",
signingKeys: [],
tokenEndpoint: "https://auth.matrix.org/oauth2/token",
tokenEndpoint: "https://auth.matrix.org/token",
}),
warning: undefined,
warning: null,
});
});
});
Expand Down
Loading