Compare commits

...

7 Commits

Author SHA1 Message Date
Michael Telatynski
0e36c24f94 Iterate
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-01-20 12:35:57 +00:00
Michael Telatynski
8c4c94bc92 Simplify the world
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-01-20 10:38:22 +00:00
Michael Telatynski
053003c717 Merge remote-tracking branch 'origin/t3chguy/oidc-auth-metdata' into t3chguy/oidc-auth-metdata 2025-01-17 15:44:19 +00:00
Michael Telatynski
e4dd805939 Iterate
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-01-17 15:44:12 +00:00
Michael Telatynski
c20d8dce70 Merge branch 'develop' into t3chguy/oidc-auth-metdata 2025-01-17 15:23:51 +00:00
Michael Telatynski
709f63dd86 Update tests
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-01-17 15:18:58 +00:00
Michael Telatynski
0830c878f7 Switch OIDC primarily to new /auth_metadata API
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-01-17 15:01:25 +00:00
17 changed files with 91 additions and 148 deletions

View File

@@ -31,8 +31,7 @@ export function shouldShowQr(
): boolean { ): boolean {
const msc4108Supported = !!versions?.unstable_features?.["org.matrix.msc4108"]; const msc4108Supported = !!versions?.unstable_features?.["org.matrix.msc4108"];
const deviceAuthorizationGrantSupported = const deviceAuthorizationGrantSupported = oidcClientConfig?.grant_types_supported.includes(DEVICE_CODE_SCOPE);
oidcClientConfig?.metadata?.grant_types_supported.includes(DEVICE_CODE_SCOPE);
return ( return (
!!deviceAuthorizationGrantSupported && !!deviceAuthorizationGrantSupported &&

View File

@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/ */
import React, { lazy, Suspense, useCallback, useContext, useEffect, useRef, useState } from "react"; import React, { lazy, Suspense, useCallback, useContext, useEffect, useRef, useState } from "react";
import { discoverAndValidateOIDCIssuerWellKnown, MatrixClient } from "matrix-js-sdk/src/matrix"; import { MatrixClient } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { defer } from "matrix-js-sdk/src/utils"; import { defer } from "matrix-js-sdk/src/utils";
@@ -163,10 +163,7 @@ const SessionManagerTab: React.FC<{
const clientVersions = useAsyncMemo(() => matrixClient.getVersions(), [matrixClient]); const clientVersions = useAsyncMemo(() => matrixClient.getVersions(), [matrixClient]);
const oidcClientConfig = useAsyncMemo(async () => { const oidcClientConfig = useAsyncMemo(async () => {
try { try {
const authIssuer = await matrixClient?.getAuthIssuer(); return await matrixClient?.getAuthMetadata();
if (authIssuer) {
return discoverAndValidateOIDCIssuerWellKnown(authIssuer.issuer);
}
} catch (e) { } catch (e) {
logger.error("Failed to discover OIDC metadata", e); logger.error("Failed to discover OIDC metadata", e);
} }

View File

@@ -50,11 +50,8 @@ export class OidcClientStore {
} else { } else {
// We are not in OIDC Native mode, as we have no locally stored issuer. Check if the server delegates auth to OIDC. // We are not in OIDC Native mode, as we have no locally stored issuer. Check if the server delegates auth to OIDC.
try { try {
const authIssuer = await this.matrixClient.getAuthIssuer(); const authMetadata = await this.matrixClient.getAuthMetadata();
const { accountManagementEndpoint, metadata } = await discoverAndValidateOIDCIssuerWellKnown( this.setAccountManagementEndpoint(authMetadata.account_management_uri, authMetadata.issuer);
authIssuer.issuer,
);
this.setAccountManagementEndpoint(accountManagementEndpoint, metadata.issuer);
} catch (e) { } catch (e) {
console.log("Auth issuer not found", e); console.log("Auth issuer not found", e);
} }
@@ -153,14 +150,11 @@ export class OidcClientStore {
try { try {
const clientId = getStoredOidcClientId(); const clientId = getStoredOidcClientId();
const { accountManagementEndpoint, metadata, signingKeys } = await discoverAndValidateOIDCIssuerWellKnown( const authMetadata = await discoverAndValidateOIDCIssuerWellKnown(this.authenticatedIssuer);
this.authenticatedIssuer, this.setAccountManagementEndpoint(authMetadata.account_management_uri, authMetadata.issuer);
);
this.setAccountManagementEndpoint(accountManagementEndpoint, metadata.issuer);
this.oidcClient = new OidcClient({ this.oidcClient = new OidcClient({
...metadata, authority: authMetadata.issuer,
authority: metadata.issuer, signingKeys: authMetadata.signingKeys ?? undefined,
signingKeys,
redirect_uri: PlatformPeg.get()!.getOidcCallbackUrl().href, redirect_uri: PlatformPeg.get()!.getOidcCallbackUrl().href,
client_id: clientId, client_id: clientId,
}); });

View File

@@ -11,7 +11,6 @@ import {
AutoDiscovery, AutoDiscovery,
AutoDiscoveryError, AutoDiscoveryError,
ClientConfig, ClientConfig,
discoverAndValidateOIDCIssuerWellKnown,
IClientWellKnown, IClientWellKnown,
MatrixClient, MatrixClient,
MatrixError, MatrixError,
@@ -293,8 +292,7 @@ export default class AutoDiscoveryUtils {
let delegatedAuthenticationError: Error | undefined; let delegatedAuthenticationError: Error | undefined;
try { try {
const tempClient = new MatrixClient({ baseUrl: preferredHomeserverUrl }); const tempClient = new MatrixClient({ baseUrl: preferredHomeserverUrl });
const { issuer } = await tempClient.getAuthIssuer(); delegatedAuthentication = await tempClient.getAuthMetadata();
delegatedAuthentication = await discoverAndValidateOIDCIssuerWellKnown(issuer);
} catch (e) { } catch (e) {
if (e instanceof MatrixError && e.httpStatus === 404 && e.errcode === "M_UNRECOGNIZED") { if (e instanceof MatrixError && e.httpStatus === 404 && e.errcode === "M_UNRECOGNIZED") {
// 404 M_UNRECOGNIZED means the server does not support OIDC // 404 M_UNRECOGNIZED means the server does not support OIDC

View File

@@ -39,7 +39,7 @@ export const startOidcLogin = async (
const prompt = isRegistration ? "create" : undefined; const prompt = isRegistration ? "create" : undefined;
const authorizationUrl = await generateOidcAuthorizationUrl({ const authorizationUrl = await generateOidcAuthorizationUrl({
metadata: delegatedAuthConfig.metadata, metadata: delegatedAuthConfig,
redirectUri, redirectUri,
clientId, clientId,
homeserverUrl, homeserverUrl,

View File

@@ -15,8 +15,6 @@ import { OidcClientConfig } from "matrix-js-sdk/src/matrix";
* @returns whether user registration is supported * @returns whether user registration is supported
*/ */
export const isUserRegistrationSupported = (delegatedAuthConfig: OidcClientConfig): boolean => { export const isUserRegistrationSupported = (delegatedAuthConfig: OidcClientConfig): boolean => {
// The OidcMetadata type from oidc-client-ts does not include `prompt_values_supported` const supportedPrompts = delegatedAuthConfig.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"); return Array.isArray(supportedPrompts) && supportedPrompts?.includes("create");
}; };

View File

@@ -40,9 +40,9 @@ export const getOidcClientId = async (
delegatedAuthConfig: OidcClientConfig, delegatedAuthConfig: OidcClientConfig,
staticOidcClients?: IConfigOptions["oidc_static_clients"], staticOidcClients?: IConfigOptions["oidc_static_clients"],
): Promise<string> => { ): Promise<string> => {
const staticClientId = getStaticOidcClientId(delegatedAuthConfig.metadata.issuer, staticOidcClients); const staticClientId = getStaticOidcClientId(delegatedAuthConfig.issuer, staticOidcClients);
if (staticClientId) { if (staticClientId) {
logger.debug(`Using static clientId for issuer ${delegatedAuthConfig.metadata.issuer}`); logger.debug(`Using static clientId for issuer ${delegatedAuthConfig.issuer}`);
return staticClientId; return staticClientId;
} }
return await registerOidcClient(delegatedAuthConfig, await PlatformPeg.get()!.getOidcClientMetadata()); return await registerOidcClient(delegatedAuthConfig, await PlatformPeg.get()!.getOidcClientMetadata());

View File

@@ -6,41 +6,4 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details. Please see LICENSE files in the repository root for full details.
*/ */
import { OidcClientConfig } from "matrix-js-sdk/src/matrix"; export { makeDelegatedAuthConfig, mockOpenIdConfiguration } from "matrix-js-sdk/src/testing";
import { ValidatedIssuerMetadata } from "matrix-js-sdk/src/oidc/validate";
/**
* Makes a valid OidcClientConfig with minimum valid values
* @param issuer used as the base for all other urls
* @returns OidcClientConfig
*/
export const makeDelegatedAuthConfig = (issuer = "https://auth.org/"): OidcClientConfig => {
const metadata = mockOpenIdConfiguration(issuer);
return {
accountManagementEndpoint: issuer + "account",
registrationEndpoint: metadata.registration_endpoint,
authorizationEndpoint: metadata.authorization_endpoint,
tokenEndpoint: metadata.token_endpoint,
metadata,
};
};
/**
* Useful for mocking <issuer>/.well-known/openid-configuration
* @param issuer used as the base for all other urls
* @returns ValidatedIssuerMetadata
*/
export const mockOpenIdConfiguration = (issuer = "https://auth.org/"): ValidatedIssuerMetadata => ({
issuer,
revocation_endpoint: issuer + "revoke",
token_endpoint: issuer + "token",
authorization_endpoint: issuer + "auth",
registration_endpoint: issuer + "registration",
device_authorization_endpoint: issuer + "device",
jwks_uri: issuer + "jwks",
response_types_supported: ["code"],
grant_types_supported: ["authorization_code", "refresh_token"],
code_challenge_methods_supported: ["S256"],
account_management_uri: issuer + "account",
});

View File

@@ -749,11 +749,8 @@ describe("Lifecycle", () => {
"eyJhbGciOiJSUzI1NiIsImtpZCI6Imh4ZEhXb0Y5bW4ifQ.eyJzdWIiOiIwMUhQUDJGU0JZREU5UDlFTU04REQ3V1pIUiIsImlzcyI6Imh0dHBzOi8vYXV0aC1vaWRjLmxhYi5lbGVtZW50LmRldi8iLCJpYXQiOjE3MTUwNzE5ODUsImF1dGhfdGltZSI6MTcwNzk5MDMxMiwiY19oYXNoIjoidGt5R1RhUjU5aTk3YXoyTU4yMGdidyIsImV4cCI6MTcxNTA3NTU4NSwibm9uY2UiOiJxaXhwM0hFMmVaIiwiYXVkIjoiMDFIWDk0Mlg3QTg3REgxRUs2UDRaNjI4WEciLCJhdF9oYXNoIjoiNFlFUjdPRlVKTmRTeEVHV2hJUDlnZyJ9.HxODneXvSTfWB5Vc4cf7b8GiN2gdwUuTiyVqZuupWske2HkZiJZUt5Lsxg9BW3gz28POkE0Ln17snlkmy02B_AD3DQxKOOxQCzIIARHdfFvZxgGWsMdFcVQZDW7rtXcqgj-SpVaUQ_8acsgxSrz_DF2o0O4tto0PT6wVUiw8KlBmgWTscWPeAWe-39T-8EiQ8Wi16h6oSPcz2NzOQ7eOM_S9fDkOorgcBkRGLl1nrahrPSdWJSGAeruk5mX4YxN714YThFDyEA2t9YmKpjaiSQ2tT-Xkd7tgsZqeirNs2ni9mIiFX3bRX6t2AhUNzA7MaX9ZyizKGa6go3BESO_oDg"; "eyJhbGciOiJSUzI1NiIsImtpZCI6Imh4ZEhXb0Y5bW4ifQ.eyJzdWIiOiIwMUhQUDJGU0JZREU5UDlFTU04REQ3V1pIUiIsImlzcyI6Imh0dHBzOi8vYXV0aC1vaWRjLmxhYi5lbGVtZW50LmRldi8iLCJpYXQiOjE3MTUwNzE5ODUsImF1dGhfdGltZSI6MTcwNzk5MDMxMiwiY19oYXNoIjoidGt5R1RhUjU5aTk3YXoyTU4yMGdidyIsImV4cCI6MTcxNTA3NTU4NSwibm9uY2UiOiJxaXhwM0hFMmVaIiwiYXVkIjoiMDFIWDk0Mlg3QTg3REgxRUs2UDRaNjI4WEciLCJhdF9oYXNoIjoiNFlFUjdPRlVKTmRTeEVHV2hJUDlnZyJ9.HxODneXvSTfWB5Vc4cf7b8GiN2gdwUuTiyVqZuupWske2HkZiJZUt5Lsxg9BW3gz28POkE0Ln17snlkmy02B_AD3DQxKOOxQCzIIARHdfFvZxgGWsMdFcVQZDW7rtXcqgj-SpVaUQ_8acsgxSrz_DF2o0O4tto0PT6wVUiw8KlBmgWTscWPeAWe-39T-8EiQ8Wi16h6oSPcz2NzOQ7eOM_S9fDkOorgcBkRGLl1nrahrPSdWJSGAeruk5mX4YxN714YThFDyEA2t9YmKpjaiSQ2tT-Xkd7tgsZqeirNs2ni9mIiFX3bRX6t2AhUNzA7MaX9ZyizKGa6go3BESO_oDg";
beforeAll(() => { beforeAll(() => {
fetchMock.get( fetchMock.get(`${delegatedAuthConfig.issuer}.well-known/openid-configuration`, delegatedAuthConfig);
`${delegatedAuthConfig.metadata.issuer}.well-known/openid-configuration`, fetchMock.get(`${delegatedAuthConfig.issuer}jwks`, {
delegatedAuthConfig.metadata,
);
fetchMock.get(`${delegatedAuthConfig.metadata.issuer}jwks`, {
status: 200, status: 200,
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -772,9 +769,7 @@ describe("Lifecycle", () => {
await setLoggedIn(credentials); await setLoggedIn(credentials);
// didn't try to initialise token refresher // didn't try to initialise token refresher
expect(fetchMock).not.toHaveFetched( expect(fetchMock).not.toHaveFetched(`${delegatedAuthConfig.issuer}.well-known/openid-configuration`);
`${delegatedAuthConfig.metadata.issuer}.well-known/openid-configuration`,
);
}); });
it("should not try to create a token refresher without a deviceId", async () => { it("should not try to create a token refresher without a deviceId", async () => {
@@ -785,9 +780,7 @@ describe("Lifecycle", () => {
}); });
// didn't try to initialise token refresher // didn't try to initialise token refresher
expect(fetchMock).not.toHaveFetched( expect(fetchMock).not.toHaveFetched(`${delegatedAuthConfig.issuer}.well-known/openid-configuration`);
`${delegatedAuthConfig.metadata.issuer}.well-known/openid-configuration`,
);
}); });
it("should not try to create a token refresher without an issuer in session storage", async () => { it("should not try to create a token refresher without an issuer in session storage", async () => {
@@ -803,9 +796,7 @@ describe("Lifecycle", () => {
}); });
// didn't try to initialise token refresher // didn't try to initialise token refresher
expect(fetchMock).not.toHaveFetched( expect(fetchMock).not.toHaveFetched(`${delegatedAuthConfig.issuer}.well-known/openid-configuration`);
`${delegatedAuthConfig.metadata.issuer}.well-known/openid-configuration`,
);
}); });
it("should create a client with a tokenRefreshFunction", async () => { it("should create a client with a tokenRefreshFunction", async () => {

View File

@@ -384,7 +384,7 @@ describe("Login", function () {
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…")); await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
// didn't try to register // didn't try to register
expect(fetchMock).not.toHaveBeenCalledWith(delegatedAuth.registrationEndpoint); expect(fetchMock).not.toHaveBeenCalledWith(delegatedAuth.registration_endpoint);
// continued with normal setup // continued with normal setup
expect(mockClient.loginFlows).toHaveBeenCalled(); expect(mockClient.loginFlows).toHaveBeenCalled();
// normal password login rendered // normal password login rendered
@@ -394,25 +394,25 @@ 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.registration_endpoint!, { status: 500 });
getComponent(hsUrl, isUrl, delegatedAuth); getComponent(hsUrl, isUrl, delegatedAuth);
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…")); await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
// tried to register // tried to register
expect(fetchMock).toHaveBeenCalledWith(delegatedAuth.registrationEndpoint, expect.any(Object)); expect(fetchMock).toHaveBeenCalledWith(delegatedAuth.registration_endpoint, expect.any(Object));
// called with values from config // called with values from config
expect(registerClientUtils.getOidcClientId).toHaveBeenCalledWith(delegatedAuth, oidcStaticClientsConfig); expect(registerClientUtils.getOidcClientId).toHaveBeenCalledWith(delegatedAuth, oidcStaticClientsConfig);
}); });
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.registration_endpoint!, { status: 500 });
getComponent(hsUrl, isUrl, delegatedAuth); getComponent(hsUrl, isUrl, delegatedAuth);
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…")); await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
// tried to register // tried to register
expect(fetchMock).toHaveBeenCalledWith(delegatedAuth.registrationEndpoint, expect.any(Object)); expect(fetchMock).toHaveBeenCalledWith(delegatedAuth.registration_endpoint, expect.any(Object));
expect(logger.error).toHaveBeenCalledWith(new Error(OidcError.DynamicRegistrationFailed)); expect(logger.error).toHaveBeenCalledWith(new Error(OidcError.DynamicRegistrationFailed));
// continued with normal setup // continued with normal setup
@@ -423,7 +423,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.registration_endpoint!, { client_id: "abc123" });
getComponent(hsUrl, isUrl, delegatedAuth); getComponent(hsUrl, isUrl, delegatedAuth);
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…")); await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
@@ -455,7 +455,7 @@ describe("Login", function () {
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…")); await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
// didn't try to register // didn't try to register
expect(fetchMock).not.toHaveBeenCalledWith(delegatedAuth.registrationEndpoint); expect(fetchMock).not.toHaveBeenCalledWith(delegatedAuth.registration_endpoint);
// continued with normal setup // continued with normal setup
expect(mockClient.loginFlows).toHaveBeenCalled(); expect(mockClient.loginFlows).toHaveBeenCalled();
// oidc-aware 'continue' button displayed // oidc-aware 'continue' button displayed

View File

@@ -158,24 +158,26 @@ describe("Registration", function () {
describe("when delegated authentication is configured and enabled", () => { describe("when delegated authentication is configured and enabled", () => {
const authConfig = makeDelegatedAuthConfig(); const authConfig = makeDelegatedAuthConfig();
const clientId = "test-client-id"; const clientId = "test-client-id";
// @ts-ignore authConfig.prompt_values_supported = ["create"];
authConfig.metadata["prompt_values_supported"] = ["create"];
beforeEach(() => { beforeEach(() => {
// mock a statically registered client to avoid dynamic registration // mock a statically registered client to avoid dynamic registration
SdkConfig.put({ SdkConfig.put({
oidc_static_clients: { oidc_static_clients: {
[authConfig.metadata.issuer]: { [authConfig.issuer]: {
client_id: clientId, client_id: clientId,
}, },
}, },
}); });
fetchMock.get(`${defaultHsUrl}/_matrix/client/unstable/org.matrix.msc2965/auth_issuer`, { fetchMock.get(`${defaultHsUrl}/_matrix/client/unstable/org.matrix.msc2965/auth_issuer`, {
issuer: authConfig.metadata.issuer, issuer: authConfig.issuer,
}); });
fetchMock.get("https://auth.org/.well-known/openid-configuration", authConfig.metadata); fetchMock.get("https://auth.org/.well-known/openid-configuration", {
fetchMock.get(authConfig.metadata.jwks_uri!, { keys: [] }); ...authConfig,
signingKeys: undefined,
});
fetchMock.get(authConfig.jwks_uri!, { keys: [] });
}); });
it("should display oidc-native continue button", async () => { it("should display oidc-native continue button", async () => {

View File

@@ -57,7 +57,7 @@ import SettingsStore from "../../../../../../../src/settings/SettingsStore";
import { getClientInformationEventType } from "../../../../../../../src/utils/device/clientInformation"; import { getClientInformationEventType } from "../../../../../../../src/utils/device/clientInformation";
import { SDKContext, SdkContextClass } from "../../../../../../../src/contexts/SDKContext"; import { SDKContext, SdkContextClass } from "../../../../../../../src/contexts/SDKContext";
import { OidcClientStore } from "../../../../../../../src/stores/oidc/OidcClientStore"; import { OidcClientStore } from "../../../../../../../src/stores/oidc/OidcClientStore";
import { mockOpenIdConfiguration } from "../../../../../../test-utils/oidc"; import { makeDelegatedAuthConfig } from "../../../../../../test-utils/oidc";
import MatrixClientContext from "../../../../../../../src/contexts/MatrixClientContext"; import MatrixClientContext from "../../../../../../../src/contexts/MatrixClientContext";
mockPlatformPeg(); mockPlatformPeg();
@@ -215,7 +215,7 @@ describe("<SessionManagerTab />", () => {
getPushers: jest.fn(), getPushers: jest.fn(),
setPusher: jest.fn(), setPusher: jest.fn(),
setLocalNotificationSettings: jest.fn(), setLocalNotificationSettings: jest.fn(),
getAuthIssuer: jest.fn().mockReturnValue(new Promise(() => {})), getAuthMetadata: jest.fn().mockRejectedValue(new MatrixError({ errcode: "M_UNRECOGNIZED" }, 404)),
}); });
jest.clearAllMocks(); jest.clearAllMocks();
jest.spyOn(logger, "error").mockRestore(); jest.spyOn(logger, "error").mockRestore();
@@ -1615,7 +1615,6 @@ describe("<SessionManagerTab />", () => {
describe("MSC4108 QR code login", () => { describe("MSC4108 QR code login", () => {
const settingsValueSpy = jest.spyOn(SettingsStore, "getValue"); const settingsValueSpy = jest.spyOn(SettingsStore, "getValue");
const issuer = "https://issuer.org"; const issuer = "https://issuer.org";
const openIdConfiguration = mockOpenIdConfiguration(issuer);
beforeEach(() => { beforeEach(() => {
settingsValueSpy.mockClear().mockReturnValue(true); settingsValueSpy.mockClear().mockReturnValue(true);
@@ -1631,16 +1630,16 @@ describe("<SessionManagerTab />", () => {
enabled: true, enabled: true,
}, },
}); });
mockClient.getAuthIssuer.mockResolvedValue({ issuer }); const delegatedAuthConfig = makeDelegatedAuthConfig(issuer);
mockCrypto.exportSecretsBundle = jest.fn(); mockClient.getAuthMetadata.mockResolvedValue({
fetchMock.mock(`${issuer}/.well-known/openid-configuration`, { ...delegatedAuthConfig,
...openIdConfiguration,
grant_types_supported: [ grant_types_supported: [
...openIdConfiguration.grant_types_supported, ...delegatedAuthConfig.grant_types_supported,
"urn:ietf:params:oauth:grant-type:device_code", "urn:ietf:params:oauth:grant-type:device_code",
], ],
}); });
fetchMock.mock(openIdConfiguration.jwks_uri!, { mockCrypto.exportSecretsBundle = jest.fn();
fetchMock.mock(delegatedAuthConfig.jwks_uri!, {
status: 200, status: 200,
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",

View File

@@ -15,7 +15,7 @@ import { OidcError } from "matrix-js-sdk/src/oidc/error";
import { OidcClientStore } from "../../../../src/stores/oidc/OidcClientStore"; import { OidcClientStore } from "../../../../src/stores/oidc/OidcClientStore";
import { flushPromises, getMockClientWithEventEmitter, mockPlatformPeg } from "../../../test-utils"; import { flushPromises, getMockClientWithEventEmitter, mockPlatformPeg } from "../../../test-utils";
import { mockOpenIdConfiguration } from "../../../test-utils/oidc"; import { makeDelegatedAuthConfig } from "../../../test-utils/oidc";
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"),
@@ -24,28 +24,30 @@ jest.mock("matrix-js-sdk/src/matrix", () => ({
describe("OidcClientStore", () => { describe("OidcClientStore", () => {
const clientId = "test-client-id"; const clientId = "test-client-id";
const metadata = mockOpenIdConfiguration(); const authConfig = makeDelegatedAuthConfig();
const account = metadata.issuer + "account"; const account = authConfig.issuer + "account";
const mockClient = getMockClientWithEventEmitter({ const mockClient = getMockClientWithEventEmitter({
getAuthIssuer: jest.fn(), getAuthMetadata: jest.fn(),
}); });
beforeEach(() => { beforeEach(() => {
localStorage.clear(); localStorage.clear();
localStorage.setItem("mx_oidc_client_id", clientId); localStorage.setItem("mx_oidc_client_id", clientId);
localStorage.setItem("mx_oidc_token_issuer", metadata.issuer); localStorage.setItem("mx_oidc_token_issuer", authConfig.issuer);
mocked(discoverAndValidateOIDCIssuerWellKnown).mockClear().mockResolvedValue({ mocked(discoverAndValidateOIDCIssuerWellKnown)
metadata, .mockClear()
accountManagementEndpoint: account, .mockResolvedValue({
authorizationEndpoint: "authorization-endpoint", ...authConfig,
tokenEndpoint: "token-endpoint", account_management_uri: account,
authorization_endpoint: "authorization-endpoint",
token_endpoint: "token-endpoint",
}); });
jest.spyOn(logger, "error").mockClear(); jest.spyOn(logger, "error").mockClear();
fetchMock.get(`${metadata.issuer}.well-known/openid-configuration`, metadata); fetchMock.get(`${authConfig.issuer}.well-known/openid-configuration`, authConfig);
fetchMock.get(`${metadata.issuer}jwks`, { keys: [] }); fetchMock.get(`${authConfig.issuer}jwks`, { keys: [] });
mockPlatformPeg(); mockPlatformPeg();
}); });
@@ -116,7 +118,7 @@ describe("OidcClientStore", () => {
const client = await store.getOidcClient(); const client = await store.getOidcClient();
expect(client?.settings.client_id).toEqual(clientId); expect(client?.settings.client_id).toEqual(clientId);
expect(client?.settings.authority).toEqual(metadata.issuer); expect(client?.settings.authority).toEqual(authConfig.issuer);
}); });
it("should set account management endpoint when configured", async () => { it("should set account management endpoint when configured", async () => {
@@ -129,17 +131,19 @@ describe("OidcClientStore", () => {
}); });
it("should set account management endpoint to issuer when not configured", async () => { it("should set account management endpoint to issuer when not configured", async () => {
mocked(discoverAndValidateOIDCIssuerWellKnown).mockClear().mockResolvedValue({ mocked(discoverAndValidateOIDCIssuerWellKnown)
metadata, .mockClear()
accountManagementEndpoint: undefined, .mockResolvedValue({
authorizationEndpoint: "authorization-endpoint", ...authConfig,
tokenEndpoint: "token-endpoint", account_management_uri: undefined,
authorization_endpoint: "authorization-endpoint",
token_endpoint: "token-endpoint",
}); });
const store = new OidcClientStore(mockClient); const store = new OidcClientStore(mockClient);
await store.readyPromise; await store.readyPromise;
expect(store.accountManagementEndpoint).toEqual(metadata.issuer); expect(store.accountManagementEndpoint).toEqual(authConfig.issuer);
}); });
it("should reuse initialised oidc client", async () => { it("should reuse initialised oidc client", async () => {
@@ -175,7 +179,7 @@ describe("OidcClientStore", () => {
fetchMock.resetHistory(); fetchMock.resetHistory();
fetchMock.post( fetchMock.post(
metadata.revocation_endpoint, authConfig.revocation_endpoint,
{ {
status: 200, status: 200,
}, },
@@ -197,7 +201,7 @@ describe("OidcClientStore", () => {
await store.revokeTokens(accessToken, refreshToken); await store.revokeTokens(accessToken, refreshToken);
expect(fetchMock).toHaveFetchedTimes(2, metadata.revocation_endpoint); expect(fetchMock).toHaveFetchedTimes(2, authConfig.revocation_endpoint);
expect(OidcClient.prototype.revokeToken).toHaveBeenCalledWith(accessToken, "access_token"); expect(OidcClient.prototype.revokeToken).toHaveBeenCalledWith(accessToken, "access_token");
expect(OidcClient.prototype.revokeToken).toHaveBeenCalledWith(refreshToken, "refresh_token"); expect(OidcClient.prototype.revokeToken).toHaveBeenCalledWith(refreshToken, "refresh_token");
}); });
@@ -206,14 +210,14 @@ describe("OidcClientStore", () => {
// fail once, then succeed // fail once, then succeed
fetchMock fetchMock
.postOnce( .postOnce(
metadata.revocation_endpoint, authConfig.revocation_endpoint,
{ {
status: 404, status: 404,
}, },
{ overwriteRoutes: true, sendAsJson: true }, { overwriteRoutes: true, sendAsJson: true },
) )
.post( .post(
metadata.revocation_endpoint, authConfig.revocation_endpoint,
{ {
status: 200, status: 200,
}, },
@@ -226,7 +230,7 @@ describe("OidcClientStore", () => {
"Failed to revoke tokens", "Failed to revoke tokens",
); );
expect(fetchMock).toHaveFetchedTimes(2, metadata.revocation_endpoint); expect(fetchMock).toHaveFetchedTimes(2, authConfig.revocation_endpoint);
expect(OidcClient.prototype.revokeToken).toHaveBeenCalledWith(accessToken, "access_token"); expect(OidcClient.prototype.revokeToken).toHaveBeenCalledWith(accessToken, "access_token");
}); });
}); });
@@ -237,7 +241,10 @@ describe("OidcClientStore", () => {
}); });
it("should resolve account management endpoint", async () => { it("should resolve account management endpoint", async () => {
mockClient.getAuthIssuer.mockResolvedValue({ issuer: metadata.issuer }); mockClient.getAuthMetadata.mockResolvedValue({
...authConfig,
account_management_uri: account,
});
const store = new OidcClientStore(mockClient); const store = new OidcClientStore(mockClient);
await store.readyPromise; await store.readyPromise;
expect(store.accountManagementEndpoint).toBe(account); expect(store.accountManagementEndpoint).toBe(account);

View File

@@ -355,21 +355,19 @@ describe("AutoDiscoveryUtils", () => {
hsNameIsDifferent: true, hsNameIsDifferent: true,
hsName: serverName, hsName: serverName,
delegatedAuthentication: expect.objectContaining({ delegatedAuthentication: expect.objectContaining({
accountManagementActionsSupported: [ issuer,
account_management_actions_supported: [
"org.matrix.profile", "org.matrix.profile",
"org.matrix.sessions_list", "org.matrix.sessions_list",
"org.matrix.session_view", "org.matrix.session_view",
"org.matrix.session_end", "org.matrix.session_end",
"org.matrix.cross_signing_reset", "org.matrix.cross_signing_reset",
], ],
accountManagementEndpoint: "https://auth.matrix.org/account/", account_management_uri: "https://auth.matrix.org/account/",
authorizationEndpoint: "https://auth.matrix.org/auth", authorization_endpoint: "https://auth.matrix.org/auth",
metadata: expect.objectContaining({ registration_endpoint: "https://auth.matrix.org/registration",
issuer,
}),
registrationEndpoint: "https://auth.matrix.org/registration",
signingKeys: [], signingKeys: [],
tokenEndpoint: "https://auth.matrix.org/token", token_endpoint: "https://auth.matrix.org/token",
}), }),
warning: null, warning: null,
}); });

View File

@@ -38,7 +38,7 @@ describe("TokenRefresher", () => {
}; };
beforeEach(() => { beforeEach(() => {
fetchMock.get(`${issuer}.well-known/openid-configuration`, authConfig.metadata); fetchMock.get(`${issuer}.well-known/openid-configuration`, authConfig);
fetchMock.get(`${issuer}jwks`, { fetchMock.get(`${issuer}jwks`, {
status: 200, status: 200,
headers: { headers: {

View File

@@ -61,10 +61,7 @@ describe("OIDC authorization", () => {
}); });
beforeAll(() => { beforeAll(() => {
fetchMock.get( fetchMock.get(`${delegatedAuthConfig.issuer}.well-known/openid-configuration`, delegatedAuthConfig);
`${delegatedAuthConfig.metadata.issuer}.well-known/openid-configuration`,
delegatedAuthConfig.metadata,
);
}); });
afterAll(() => { afterAll(() => {

View File

@@ -58,7 +58,7 @@ describe("getOidcClientId()", () => {
const authConfigWithoutRegistration: OidcClientConfig = makeDelegatedAuthConfig( const authConfigWithoutRegistration: OidcClientConfig = makeDelegatedAuthConfig(
"https://issuerWithoutStaticClientId.org/", "https://issuerWithoutStaticClientId.org/",
); );
authConfigWithoutRegistration.registrationEndpoint = undefined; authConfigWithoutRegistration.registration_endpoint = undefined;
await expect(getOidcClientId(authConfigWithoutRegistration, staticOidcClients)).rejects.toThrow( await expect(getOidcClientId(authConfigWithoutRegistration, staticOidcClients)).rejects.toThrow(
OidcError.DynamicRegistrationNotSupported, OidcError.DynamicRegistrationNotSupported,
); );
@@ -69,7 +69,7 @@ describe("getOidcClientId()", () => {
it("should handle when staticOidcClients object is falsy", async () => { it("should handle when staticOidcClients object is falsy", async () => {
const authConfigWithoutRegistration: OidcClientConfig = { const authConfigWithoutRegistration: OidcClientConfig = {
...delegatedAuthConfig, ...delegatedAuthConfig,
registrationEndpoint: undefined, registration_endpoint: undefined,
}; };
await expect(getOidcClientId(authConfigWithoutRegistration)).rejects.toThrow( await expect(getOidcClientId(authConfigWithoutRegistration)).rejects.toThrow(
OidcError.DynamicRegistrationNotSupported, OidcError.DynamicRegistrationNotSupported,
@@ -79,14 +79,14 @@ describe("getOidcClientId()", () => {
}); });
it("should make correct request to register client", async () => { it("should make correct request to register client", async () => {
fetchMockJest.post(delegatedAuthConfig.registrationEndpoint!, { fetchMockJest.post(delegatedAuthConfig.registration_endpoint!, {
status: 200, status: 200,
body: JSON.stringify({ client_id: dynamicClientId }), body: JSON.stringify({ client_id: dynamicClientId }),
}); });
expect(await getOidcClientId(delegatedAuthConfig)).toEqual(dynamicClientId); expect(await getOidcClientId(delegatedAuthConfig)).toEqual(dynamicClientId);
// didn't try to register // didn't try to register
expect(fetchMockJest).toHaveBeenCalledWith( expect(fetchMockJest).toHaveBeenCalledWith(
delegatedAuthConfig.registrationEndpoint!, delegatedAuthConfig.registration_endpoint!,
expect.objectContaining({ expect.objectContaining({
headers: { headers: {
"Accept": "application/json", "Accept": "application/json",
@@ -111,14 +111,14 @@ describe("getOidcClientId()", () => {
}); });
it("should throw when registration request fails", async () => { it("should throw when registration request fails", async () => {
fetchMockJest.post(delegatedAuthConfig.registrationEndpoint!, { fetchMockJest.post(delegatedAuthConfig.registration_endpoint!, {
status: 500, status: 500,
}); });
await expect(getOidcClientId(delegatedAuthConfig)).rejects.toThrow(OidcError.DynamicRegistrationFailed); await expect(getOidcClientId(delegatedAuthConfig)).rejects.toThrow(OidcError.DynamicRegistrationFailed);
}); });
it("should throw when registration response is invalid", async () => { it("should throw when registration response is invalid", async () => {
fetchMockJest.post(delegatedAuthConfig.registrationEndpoint!, { fetchMockJest.post(delegatedAuthConfig.registration_endpoint!, {
status: 200, status: 200,
// no clientId in response // no clientId in response
body: "{}", body: "{}",