Update MSC2965 OIDC Discovery implementation (#12245)

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
This commit is contained in:
Michael Telatynski 2024-02-23 16:43:14 +00:00 committed by GitHub
parent 729eca49e4
commit 7b1e8e3d2f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 350 additions and 300 deletions

View file

@ -795,7 +795,7 @@ async function createOidcTokenRefresher(credentials: IMatrixClientCreds): Promis
throw new Error("Expected deviceId in user credentials."); throw new Error("Expected deviceId in user credentials.");
} }
const tokenRefresher = new TokenRefresher( const tokenRefresher = new TokenRefresher(
{ issuer: tokenIssuer }, tokenIssuer,
clientId, clientId,
redirectUri, redirectUri,
deviceId, deviceId,

View file

@ -80,9 +80,6 @@ export default class ServerPickerDialog extends React.PureComponent<IProps, ISta
this.setState({ otherHomeserver: ev.target.value }); this.setState({ otherHomeserver: ev.target.value });
}; };
// TODO: Do we want to support .well-known lookups here?
// If for some reason someone enters "matrix.org" for a URL, we could do a lookup to
// find their homeserver without demanding they use "https://matrix.org"
private validate = withValidation<this, { error?: string }>({ private validate = withValidation<this, { error?: string }>({
deriveData: async ({ value }): Promise<{ error?: string }> => { deriveData: async ({ value }): Promise<{ error?: string }> => {
let hsUrl = (value ?? "").trim(); // trim to account for random whitespace let hsUrl = (value ?? "").trim(); // trim to account for random whitespace
@ -91,7 +88,10 @@ export default class ServerPickerDialog extends React.PureComponent<IProps, ISta
if (!hsUrl.includes("://")) { if (!hsUrl.includes("://")) {
try { try {
const discoveryResult = await AutoDiscovery.findClientConfig(hsUrl); const discoveryResult = await AutoDiscovery.findClientConfig(hsUrl);
this.validatedConf = AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(hsUrl, discoveryResult); this.validatedConf = await AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(
hsUrl,
discoveryResult,
);
return {}; // we have a validated config, we don't need to try the other paths return {}; // we have a validated config, we don't need to try the other paths
} catch (e) { } catch (e) {
logger.error(`Attempted ${hsUrl} as a server_name but it failed`, e); logger.error(`Attempted ${hsUrl} as a server_name but it failed`, e);

View file

@ -14,18 +14,23 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { IDelegatedAuthConfig, MatrixClient, M_AUTHENTICATION } from "matrix-js-sdk/src/matrix"; import { MatrixClient, discoverAndValidateOIDCIssuerWellKnown } from "matrix-js-sdk/src/matrix";
import { discoverAndValidateAuthenticationConfig } from "matrix-js-sdk/src/oidc/discovery";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { OidcClient } from "oidc-client-ts"; import { OidcClient } from "oidc-client-ts";
import { getStoredOidcTokenIssuer, getStoredOidcClientId } from "../../utils/oidc/persistOidcSettings"; import { getStoredOidcTokenIssuer, getStoredOidcClientId } from "../../utils/oidc/persistOidcSettings";
import { getDelegatedAuthAccountUrl } from "../../utils/oidc/getDelegatedAuthAccountUrl";
import PlatformPeg from "../../PlatformPeg"; import PlatformPeg from "../../PlatformPeg";
/** /**
* @experimental * @experimental
* Stores information about configured OIDC provider * 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. Auth is then in Legacy mode and everything uses normal Matrix APIs.
*/ */
export class OidcClientStore { export class OidcClientStore {
private oidcClient?: OidcClient; private oidcClient?: OidcClient;
@ -47,8 +52,16 @@ export class OidcClientStore {
if (this.authenticatedIssuer) { if (this.authenticatedIssuer) {
await this.getOidcClient(); await this.getOidcClient();
} else { } else {
const wellKnown = await this.matrixClient.waitForClientWellKnown(); // We are not in OIDC Native mode, as we have no locally stored issuer. Check if the server delegates auth to OIDC.
this._accountManagementEndpoint = getDelegatedAuthAccountUrl(wellKnown); try {
const authIssuer = await this.matrixClient.getAuthIssuer();
const { accountManagementEndpoint, metadata } = await discoverAndValidateOIDCIssuerWellKnown(
authIssuer.issuer,
);
this._accountManagementEndpoint = accountManagementEndpoint ?? metadata.issuer;
} catch (e) {
console.log("Auth issuer not found", e);
}
} }
} }
@ -127,23 +140,18 @@ export class OidcClientStore {
* @returns promise that resolves when initialising OidcClient succeeds or fails * @returns promise that resolves when initialising OidcClient succeeds or fails
*/ */
private async initOidcClient(): Promise<void> { private async initOidcClient(): Promise<void> {
const wellKnown = await this.matrixClient.waitForClientWellKnown(); if (!this.authenticatedIssuer) {
if (!wellKnown && !this.authenticatedIssuer) {
logger.error("Cannot initialise OIDC client without issuer."); logger.error("Cannot initialise OIDC client without issuer.");
return; return;
} }
const delegatedAuthConfig =
(wellKnown && M_AUTHENTICATION.findIn<IDelegatedAuthConfig>(wellKnown)) ?? undefined;
try { try {
const clientId = getStoredOidcClientId(); const clientId = getStoredOidcClientId();
const { account, metadata, signingKeys } = await discoverAndValidateAuthenticationConfig( const { accountManagementEndpoint, metadata, signingKeys } = await discoverAndValidateOIDCIssuerWellKnown(
// if HS has valid delegated auth config in .well-known, use it this.authenticatedIssuer,
// otherwise fallback to the known issuer
delegatedAuthConfig ?? { issuer: this.authenticatedIssuer! },
); );
// if no account endpoint is configured default to the issuer // if no account endpoint is configured default to the issuer
this._accountManagementEndpoint = account ?? metadata.issuer; this._accountManagementEndpoint = accountManagementEndpoint ?? metadata.issuer;
this.oidcClient = new OidcClient({ this.oidcClient = new OidcClient({
...metadata, ...metadata,
authority: metadata.issuer, authority: metadata.issuer,

View file

@ -19,9 +19,11 @@ import {
AutoDiscovery, AutoDiscovery,
AutoDiscoveryError, AutoDiscoveryError,
ClientConfig, ClientConfig,
OidcClientConfig, discoverAndValidateOIDCIssuerWellKnown,
M_AUTHENTICATION,
IClientWellKnown, IClientWellKnown,
MatrixClient,
MatrixError,
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";
@ -217,12 +219,12 @@ export default class AutoDiscoveryUtils {
* @param {boolean} isSynthetic If true, then the discoveryResult was synthesised locally. * @param {boolean} isSynthetic If true, then the discoveryResult was synthesised locally.
* @returns {Promise<ValidatedServerConfig>} Resolves to the validated configuration. * @returns {Promise<ValidatedServerConfig>} Resolves to the validated configuration.
*/ */
public static buildValidatedConfigFromDiscovery( public static async buildValidatedConfigFromDiscovery(
serverName?: string, serverName?: string,
discoveryResult?: ClientConfig, discoveryResult?: ClientConfig,
syntaxOnly = false, syntaxOnly = false,
isSynthetic = false, isSynthetic = false,
): ValidatedServerConfig { ): Promise<ValidatedServerConfig> {
if (!discoveryResult?.["m.homeserver"]) { if (!discoveryResult?.["m.homeserver"]) {
// This shouldn't happen without major misconfiguration, so we'll log a bit of information // This shouldn't happen without major misconfiguration, so we'll log a bit of information
// in the log so we can find this bit of code but otherwise tell the user "it broke". // in the log so we can find this bit of code but otherwise tell the user "it broke".
@ -293,26 +295,20 @@ export default class AutoDiscoveryUtils {
throw new UserFriendlyError("auth|autodiscovery_unexpected_error_hs"); throw new UserFriendlyError("auth|autodiscovery_unexpected_error_hs");
} }
// 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 delegatedAuthentication: OidcClientConfig | undefined;
if (discoveryResult[M_AUTHENTICATION.stable!]?.state === AutoDiscovery.SUCCESS) { let delegatedAuthenticationError: Error | undefined;
const { try {
authorizationEndpoint, const tempClient = new MatrixClient({ baseUrl: preferredHomeserverUrl });
registrationEndpoint, const { issuer } = await tempClient.getAuthIssuer();
tokenEndpoint, delegatedAuthentication = await discoverAndValidateOIDCIssuerWellKnown(issuer);
account, } catch (e) {
issuer, if (e instanceof MatrixError && e.httpStatus === 404 && e.errcode === "M_UNRECOGNIZED") {
metadata, // 404 M_UNRECOGNIZED means the server does not support OIDC
signingKeys, } else {
} = discoveryResult[M_AUTHENTICATION.stable!] as OidcClientConfig; delegatedAuthenticationError = e as Error;
delegatedAuthentication = Object.freeze({ }
authorizationEndpoint,
registrationEndpoint,
tokenEndpoint,
account,
issuer,
metadata,
signingKeys,
});
} }
return { return {
@ -321,7 +317,7 @@ export default class AutoDiscoveryUtils {
hsNameIsDifferent: url.hostname !== preferredHomeserverName, hsNameIsDifferent: url.hostname !== preferredHomeserverName,
isUrl: preferredIdentityUrl, isUrl: preferredIdentityUrl,
isDefault: false, isDefault: false,
warning: hsResult.error, warning: hsResult.error ?? delegatedAuthenticationError ?? null,
isNameResolvable: !isSynthetic, isNameResolvable: !isSynthetic,
delegatedAuthentication, delegatedAuthentication,
} as ValidatedServerConfig; } as ValidatedServerConfig;

View file

@ -14,10 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { OidcClientConfig, IDelegatedAuthConfig } from "matrix-js-sdk/src/matrix"; import { OidcClientConfig } from "matrix-js-sdk/src/matrix";
import { ValidatedIssuerConfig } from "matrix-js-sdk/src/oidc/validate";
export type ValidatedDelegatedAuthConfig = IDelegatedAuthConfig & ValidatedIssuerConfig;
export interface ValidatedServerConfig { export interface ValidatedServerConfig {
hsUrl: string; hsUrl: string;
@ -34,9 +31,9 @@ export interface ValidatedServerConfig {
/** /**
* Config related to delegated authentication * Config related to delegated authentication
* Included when delegated auth is configured and valid, otherwise undefined * Included when delegated auth is configured and valid, otherwise undefined.
* From homeserver .well-known m.authentication, and issuer's .well-known/openid-configuration * From issuer's .well-known/openid-configuration.
* Used for OIDC native flow authentication * Used for OIDC native flow authentication.
*/ */
delegatedAuthentication?: OidcClientConfig; delegatedAuthentication?: OidcClientConfig;
} }

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { IDelegatedAuthConfig, OidcTokenRefresher, AccessTokens } from "matrix-js-sdk/src/matrix"; import { OidcTokenRefresher, AccessTokens } from "matrix-js-sdk/src/matrix";
import { IdTokenClaims } from "oidc-client-ts"; import { IdTokenClaims } from "oidc-client-ts";
import PlatformPeg from "../../PlatformPeg"; import PlatformPeg from "../../PlatformPeg";
@ -28,14 +28,14 @@ export class TokenRefresher extends OidcTokenRefresher {
private readonly deviceId!: string; private readonly deviceId!: string;
public constructor( public constructor(
authConfig: IDelegatedAuthConfig, issuer: string,
clientId: string, clientId: string,
redirectUri: string, redirectUri: string,
deviceId: string, deviceId: string,
idTokenClaims: IdTokenClaims, idTokenClaims: IdTokenClaims,
private readonly userId: string, private readonly userId: string,
) { ) {
super(authConfig, clientId, deviceId, redirectUri, idTokenClaims); super(issuer, clientId, deviceId, redirectUri, idTokenClaims);
this.deviceId = deviceId; this.deviceId = deviceId;
} }

View file

@ -1,27 +0,0 @@
/*
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 { IClientWellKnown, IDelegatedAuthConfig, M_AUTHENTICATION } from "matrix-js-sdk/src/matrix";
/**
* Get the delegated auth account management url if configured
* @param clientWellKnown from MatrixClient.getClientWellKnown
* @returns the account management url, or undefined
*/
export const getDelegatedAuthAccountUrl = (clientWellKnown: IClientWellKnown | undefined): string | undefined => {
const delegatedAuthConfig = M_AUTHENTICATION.findIn<IDelegatedAuthConfig | undefined>(clientWellKnown);
return delegatedAuthConfig?.account;
};

View file

@ -15,10 +15,9 @@ limitations under the License.
*/ */
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { registerOidcClient } from "matrix-js-sdk/src/oidc/register"; import { registerOidcClient, OidcClientConfig } from "matrix-js-sdk/src/matrix";
import { IConfigOptions } from "../../IConfigOptions"; import { IConfigOptions } from "../../IConfigOptions";
import { ValidatedDelegatedAuthConfig } from "../ValidatedServerConfig";
import PlatformPeg from "../../PlatformPeg"; import PlatformPeg from "../../PlatformPeg";
/** /**
@ -46,12 +45,12 @@ const getStaticOidcClientId = (
* @throws if no clientId is found * @throws if no clientId is found
*/ */
export const getOidcClientId = async ( export const getOidcClientId = async (
delegatedAuthConfig: ValidatedDelegatedAuthConfig, delegatedAuthConfig: OidcClientConfig,
staticOidcClients?: IConfigOptions["oidc_static_clients"], staticOidcClients?: IConfigOptions["oidc_static_clients"],
): Promise<string> => { ): Promise<string> => {
const staticClientId = getStaticOidcClientId(delegatedAuthConfig.issuer, staticOidcClients); const staticClientId = getStaticOidcClientId(delegatedAuthConfig.metadata.issuer, staticOidcClients);
if (staticClientId) { if (staticClientId) {
logger.debug(`Using static clientId for issuer ${delegatedAuthConfig.issuer}`); logger.debug(`Using static clientId for issuer ${delegatedAuthConfig.metadata.issuer}`);
return staticClientId; return staticClientId;
} }
return await registerOidcClient(delegatedAuthConfig, await PlatformPeg.get()!.getOidcClientMetadata()); return await registerOidcClient(delegatedAuthConfig, await PlatformPeg.get()!.getOidcClientMetadata());

View file

@ -683,10 +683,10 @@ describe("Lifecycle", () => {
beforeAll(() => { beforeAll(() => {
fetchMock.get( fetchMock.get(
`${delegatedAuthConfig.issuer}.well-known/openid-configuration`, `${delegatedAuthConfig.metadata.issuer}.well-known/openid-configuration`,
delegatedAuthConfig.metadata, delegatedAuthConfig.metadata,
); );
fetchMock.get(`${delegatedAuthConfig.issuer}jwks`, { fetchMock.get(`${delegatedAuthConfig.metadata.issuer}jwks`, {
status: 200, status: 200,
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@ -696,12 +696,6 @@ describe("Lifecycle", () => {
}); });
beforeEach(() => { beforeEach(() => {
// mock oidc config for oidc client initialisation
mockClient.waitForClientWellKnown.mockResolvedValue({
"m.authentication": {
issuer: issuer,
},
});
initSessionStorageMock(); initSessionStorageMock();
// set values in session storage as they would be after a successful oidc authentication // set values in session storage as they would be after a successful oidc authentication
persistOidcAuthenticatedSettings(clientId, issuer, idTokenClaims); persistOidcAuthenticatedSettings(clientId, issuer, idTokenClaims);
@ -711,7 +705,9 @@ 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(`${delegatedAuthConfig.issuer}.well-known/openid-configuration`); expect(fetchMock).not.toHaveFetched(
`${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 () => {
@ -722,7 +718,9 @@ describe("Lifecycle", () => {
}); });
// didn't try to initialise token refresher // didn't try to initialise token refresher
expect(fetchMock).not.toHaveFetched(`${delegatedAuthConfig.issuer}.well-known/openid-configuration`); expect(fetchMock).not.toHaveFetched(
`${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 () => {
@ -738,7 +736,9 @@ describe("Lifecycle", () => {
}); });
// didn't try to initialise token refresher // didn't try to initialise token refresher
expect(fetchMock).not.toHaveFetched(`${delegatedAuthConfig.issuer}.well-known/openid-configuration`); expect(fetchMock).not.toHaveFetched(
`${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

@ -211,6 +211,11 @@ describe("<MatrixChat />", () => {
unstable_features: {}, unstable_features: {},
versions: SERVER_SUPPORTED_MATRIX_VERSIONS, 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, "idbLoad").mockReset();
jest.spyOn(StorageManager, "idbSave").mockResolvedValue(undefined); jest.spyOn(StorageManager, "idbSave").mockResolvedValue(undefined);

View file

@ -37,7 +37,6 @@ jest.mock("matrix-js-sdk/src/matrix", () => ({
...jest.requireActual("matrix-js-sdk/src/matrix"), ...jest.requireActual("matrix-js-sdk/src/matrix"),
createClient: jest.fn(), createClient: jest.fn(),
})); }));
jest.useFakeTimers();
/** The matrix versions our mock server claims to support */ /** The matrix versions our mock server claims to support */
const SERVER_SUPPORTED_MATRIX_VERSIONS = ["v1.1", "v1.5", "v1.6", "v1.8", "v1.9"]; const SERVER_SUPPORTED_MATRIX_VERSIONS = ["v1.1", "v1.5", "v1.6", "v1.8", "v1.9"];
@ -160,11 +159,17 @@ describe("Registration", function () {
// 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.issuer]: { [authConfig.metadata.issuer]: {
client_id: clientId, 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", () => { describe("when oidc native flow is not enabled in settings", () => {
@ -192,14 +197,14 @@ describe("Registration", function () {
// no form // no form
expect(container.querySelector("form")).toBeFalsy(); expect(container.querySelector("form")).toBeFalsy();
expect(screen.getByText("Continue")).toBeTruthy(); expect(await screen.findByText("Continue")).toBeTruthy();
}); });
it("should start OIDC login flow as registration on button click", async () => { it("should start OIDC login flow as registration on button click", async () => {
getComponent(defaultHsUrl, defaultIsUrl, authConfig); getComponent(defaultHsUrl, defaultIsUrl, authConfig);
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…")); await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
fireEvent.click(screen.getByText("Continue")); fireEvent.click(await screen.findByText("Continue"));
expect(startOidcLogin).toHaveBeenCalledWith( expect(startOidcLogin).toHaveBeenCalledWith(
authConfig, authConfig,

View file

@ -64,6 +64,11 @@ describe("<ServerPickerDialog />", () => {
}); });
fetchMock.resetHistory(); fetchMock.resetHistory();
fetchMock.catch({
status: 404,
body: '{"errcode": "M_UNRECOGNIZED", "error": "Unrecognized request"}',
headers: { "content-type": "application/json" },
});
}); });
it("should render dialog", () => { it("should render dialog", () => {

View file

@ -17,17 +17,17 @@ limitations under the License.
import fetchMock from "fetch-mock-jest"; import fetchMock from "fetch-mock-jest";
import { mocked } from "jest-mock"; import { mocked } from "jest-mock";
import { OidcClient } from "oidc-client-ts"; import { OidcClient } from "oidc-client-ts";
import { M_AUTHENTICATION } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { discoverAndValidateAuthenticationConfig } 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 { 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 { mockOpenIdConfiguration } from "../../test-utils/oidc";
jest.mock("matrix-js-sdk/src/oidc/discovery", () => ({ jest.mock("matrix-js-sdk/src/matrix", () => ({
discoverAndValidateAuthenticationConfig: jest.fn(), ...jest.requireActual("matrix-js-sdk/src/matrix"),
discoverAndValidateOIDCIssuerWellKnown: jest.fn(),
})); }));
describe("OidcClientStore", () => { describe("OidcClientStore", () => {
@ -36,7 +36,7 @@ describe("OidcClientStore", () => {
const account = metadata.issuer + "account"; const account = metadata.issuer + "account";
const mockClient = getMockClientWithEventEmitter({ const mockClient = getMockClientWithEventEmitter({
waitForClientWellKnown: jest.fn().mockResolvedValue({}), getAuthIssuer: jest.fn(),
}); });
beforeEach(() => { beforeEach(() => {
@ -44,20 +44,16 @@ describe("OidcClientStore", () => {
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", metadata.issuer);
mocked(discoverAndValidateAuthenticationConfig).mockClear().mockResolvedValue({ mocked(discoverAndValidateOIDCIssuerWellKnown).mockClear().mockResolvedValue({
metadata, metadata,
account, accountManagementEndpoint: account,
issuer: metadata.issuer, authorizationEndpoint: "authorization-endpoint",
}); tokenEndpoint: "token-endpoint",
mockClient.waitForClientWellKnown.mockResolvedValue({
[M_AUTHENTICATION.stable!]: {
issuer: metadata.issuer,
account,
},
}); });
jest.spyOn(logger, "error").mockClear(); jest.spyOn(logger, "error").mockClear();
fetchMock.get(`${metadata.issuer}.well-known/openid-configuration`, metadata); fetchMock.get(`${metadata.issuer}.well-known/openid-configuration`, metadata);
fetchMock.get(`${metadata.issuer}jwks`, { keys: [] });
mockPlatformPeg(); mockPlatformPeg();
}); });
@ -78,7 +74,6 @@ describe("OidcClientStore", () => {
describe("initialising oidcClient", () => { describe("initialising oidcClient", () => {
it("should initialise oidc client from constructor", () => { it("should initialise oidc client from constructor", () => {
mockClient.waitForClientWellKnown.mockResolvedValue(undefined as any);
const store = new OidcClientStore(mockClient); const store = new OidcClientStore(mockClient);
// started initialising // started initialising
@ -87,7 +82,6 @@ describe("OidcClientStore", () => {
}); });
it("should fallback to stored issuer when no client well known is available", async () => { it("should fallback to stored issuer when no client well known is available", async () => {
mockClient.waitForClientWellKnown.mockResolvedValue(undefined as any);
const store = new OidcClientStore(mockClient); const store = new OidcClientStore(mockClient);
// successfully created oidc client // successfully created oidc client
@ -109,10 +103,10 @@ describe("OidcClientStore", () => {
}); });
it("should log and return when discovery and validation fails", async () => { it("should log and return when discovery and validation fails", async () => {
mocked(discoverAndValidateAuthenticationConfig).mockRejectedValue(new Error(OidcError.OpSupport)); mocked(discoverAndValidateOIDCIssuerWellKnown).mockRejectedValue(new Error(OidcError.OpSupport));
const store = new OidcClientStore(mockClient); const store = new OidcClientStore(mockClient);
await flushPromises(); await store.readyPromise;
expect(logger.error).toHaveBeenCalledWith( expect(logger.error).toHaveBeenCalledWith(
"Failed to initialise OidcClientStore", "Failed to initialise OidcClientStore",
@ -143,15 +137,15 @@ 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(discoverAndValidateAuthenticationConfig).mockClear().mockResolvedValue({ mocked(discoverAndValidateOIDCIssuerWellKnown).mockClear().mockResolvedValue({
metadata, metadata,
account: undefined, accountManagementEndpoint: undefined,
issuer: metadata.issuer, authorizationEndpoint: "authorization-endpoint",
tokenEndpoint: "token-endpoint",
}); });
const store = new OidcClientStore(mockClient); const store = new OidcClientStore(mockClient);
// @ts-ignore private property await store.readyPromise;
await store.getOidcClient();
expect(store.accountManagementEndpoint).toEqual(metadata.issuer); expect(store.accountManagementEndpoint).toEqual(metadata.issuer);
}); });
@ -175,7 +169,7 @@ describe("OidcClientStore", () => {
// only called once for multiple calls to getOidcClient // only called once for multiple calls to getOidcClient
// before and after initialisation is complete // before and after initialisation is complete
expect(discoverAndValidateAuthenticationConfig).toHaveBeenCalledTimes(1); expect(discoverAndValidateOIDCIssuerWellKnown).toHaveBeenCalledTimes(1);
}); });
}); });
@ -199,7 +193,6 @@ describe("OidcClientStore", () => {
it("should throw when oidcClient could not be initialised", async () => { it("should throw when oidcClient could not be initialised", async () => {
// make oidcClient initialisation fail // make oidcClient initialisation fail
mockClient.waitForClientWellKnown.mockResolvedValue(undefined as any);
localStorage.removeItem("mx_oidc_token_issuer"); localStorage.removeItem("mx_oidc_token_issuer");
const store = new OidcClientStore(mockClient); const store = new OidcClientStore(mockClient);
@ -245,4 +238,17 @@ describe("OidcClientStore", () => {
expect(OidcClient.prototype.revokeToken).toHaveBeenCalledWith(accessToken, "access_token"); expect(OidcClient.prototype.revokeToken).toHaveBeenCalledWith(accessToken, "access_token");
}); });
}); });
describe("OIDC Aware", () => {
beforeEach(() => {
localStorage.clear();
});
it("should resolve account management endpoint", async () => {
mockClient.getAuthIssuer.mockResolvedValue({ issuer: metadata.issuer });
const store = new OidcClientStore(mockClient);
await store.readyPromise;
expect(store.accountManagementEndpoint).toBe(account);
});
});
}); });

View file

@ -26,8 +26,7 @@ export const makeDelegatedAuthConfig = (issuer = "https://auth.org/"): OidcClien
const metadata = mockOpenIdConfiguration(issuer); const metadata = mockOpenIdConfiguration(issuer);
return { return {
issuer, accountManagementEndpoint: issuer + "account",
account: issuer + "account",
registrationEndpoint: metadata.registration_endpoint, registrationEndpoint: metadata.registration_endpoint,
authorizationEndpoint: metadata.authorization_endpoint, authorizationEndpoint: metadata.authorization_endpoint,
tokenEndpoint: metadata.token_endpoint, tokenEndpoint: metadata.token_endpoint,
@ -50,4 +49,5 @@ export const mockOpenIdConfiguration = (issuer = "https://auth.org/"): Validated
response_types_supported: ["code"], response_types_supported: ["code"],
grant_types_supported: ["authorization_code", "refresh_token"], grant_types_supported: ["authorization_code", "refresh_token"],
code_challenge_methods_supported: ["S256"], code_challenge_methods_supported: ["S256"],
account_management_uri: issuer + "account",
}); });

View file

@ -14,12 +14,22 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { AutoDiscovery, AutoDiscoveryAction, ClientConfig, M_AUTHENTICATION } from "matrix-js-sdk/src/matrix"; import { AutoDiscovery, AutoDiscoveryAction, ClientConfig } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import fetchMock from "fetch-mock-jest";
import AutoDiscoveryUtils from "../../src/utils/AutoDiscoveryUtils"; import AutoDiscoveryUtils from "../../src/utils/AutoDiscoveryUtils";
import { mockOpenIdConfiguration } from "../test-utils/oidc";
describe("AutoDiscoveryUtils", () => { describe("AutoDiscoveryUtils", () => {
beforeEach(() => {
fetchMock.catch({
status: 404,
body: '{"errcode": "M_UNRECOGNIZED", "error": "Unrecognized request"}',
headers: { "content-type": "application/json" },
});
});
describe("buildValidatedConfigFromDiscovery()", () => { describe("buildValidatedConfigFromDiscovery()", () => {
const serverName = "my-server"; const serverName = "my-server";
@ -56,24 +66,24 @@ describe("AutoDiscoveryUtils", () => {
isUrl: "identity.com", isUrl: "identity.com",
}; };
it("throws an error when discovery result is falsy", () => { it("throws an error when discovery result is falsy", async () => {
expect(() => AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, undefined as any)).toThrow( await expect(() =>
"Unexpected error resolving homeserver configuration", AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, undefined as any),
); ).rejects.toThrow("Unexpected error resolving homeserver configuration");
expect(logger.error).toHaveBeenCalled(); expect(logger.error).toHaveBeenCalled();
}); });
it("throws an error when discovery result does not include homeserver config", () => { it("throws an error when discovery result does not include homeserver config", async () => {
const discoveryResult = { const discoveryResult = {
...validIsConfig, ...validIsConfig,
} as unknown as ClientConfig; } as unknown as ClientConfig;
expect(() => AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, discoveryResult)).toThrow( await expect(() =>
"Unexpected error resolving homeserver configuration", AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, discoveryResult),
); ).rejects.toThrow("Unexpected error resolving homeserver configuration");
expect(logger.error).toHaveBeenCalled(); expect(logger.error).toHaveBeenCalled();
}); });
it("throws an error when identity server config has fail error and recognised error string", () => { it("throws an error when identity server config has fail error and recognised error string", async () => {
const discoveryResult = { const discoveryResult = {
...validHsConfig, ...validHsConfig,
"m.identity_server": { "m.identity_server": {
@ -81,13 +91,13 @@ describe("AutoDiscoveryUtils", () => {
error: "GenericFailure", error: "GenericFailure",
}, },
}; };
expect(() => AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, discoveryResult)).toThrow( await expect(() =>
"Unexpected error resolving identity server configuration", AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, discoveryResult),
); ).rejects.toThrow("Unexpected error resolving identity server configuration");
expect(logger.error).toHaveBeenCalled(); expect(logger.error).toHaveBeenCalled();
}); });
it("throws an error when homeserver config has fail error and recognised error string", () => { it("throws an error when homeserver config has fail error and recognised error string", async () => {
const discoveryResult = { const discoveryResult = {
...validIsConfig, ...validIsConfig,
"m.homeserver": { "m.homeserver": {
@ -95,25 +105,25 @@ describe("AutoDiscoveryUtils", () => {
error: AutoDiscovery.ERROR_INVALID_HOMESERVER, error: AutoDiscovery.ERROR_INVALID_HOMESERVER,
}, },
}; };
expect(() => AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, discoveryResult)).toThrow( await expect(() =>
"Homeserver URL does not appear to be a valid Matrix homeserver", AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, discoveryResult),
); ).rejects.toThrow("Homeserver URL does not appear to be a valid Matrix homeserver");
expect(logger.error).toHaveBeenCalled(); expect(logger.error).toHaveBeenCalled();
}); });
it("throws an error with fallback message identity server config has fail error", () => { it("throws an error with fallback message identity server config has fail error", async () => {
const discoveryResult = { const discoveryResult = {
...validHsConfig, ...validHsConfig,
"m.identity_server": { "m.identity_server": {
state: AutoDiscoveryAction.FAIL_ERROR, state: AutoDiscoveryAction.FAIL_ERROR,
}, },
}; };
expect(() => AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, discoveryResult)).toThrow( await expect(() =>
"Unexpected error resolving identity server configuration", AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, discoveryResult),
); ).rejects.toThrow("Unexpected error resolving identity server configuration");
}); });
it("throws an error when error is ERROR_INVALID_HOMESERVER", () => { it("throws an error when error is ERROR_INVALID_HOMESERVER", async () => {
const discoveryResult = { const discoveryResult = {
...validIsConfig, ...validIsConfig,
"m.homeserver": { "m.homeserver": {
@ -121,12 +131,12 @@ describe("AutoDiscoveryUtils", () => {
error: AutoDiscovery.ERROR_INVALID_HOMESERVER, error: AutoDiscovery.ERROR_INVALID_HOMESERVER,
}, },
}; };
expect(() => AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, discoveryResult)).toThrow( await expect(() =>
"Homeserver URL does not appear to be a valid Matrix homeserver", AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, discoveryResult),
); ).rejects.toThrow("Homeserver URL does not appear to be a valid Matrix homeserver");
}); });
it("throws an error when homeserver base_url is falsy", () => { it("throws an error when homeserver base_url is falsy", async () => {
const discoveryResult = { const discoveryResult = {
...validIsConfig, ...validIsConfig,
"m.homeserver": { "m.homeserver": {
@ -134,13 +144,13 @@ describe("AutoDiscoveryUtils", () => {
base_url: "", base_url: "",
}, },
}; };
expect(() => AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, discoveryResult)).toThrow( await expect(() =>
"Unexpected error resolving homeserver configuration", AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, discoveryResult),
); ).rejects.toThrow("Unexpected error resolving homeserver configuration");
expect(logger.error).toHaveBeenCalledWith("No homeserver URL configured"); expect(logger.error).toHaveBeenCalledWith("No homeserver URL configured");
}); });
it("throws an error when homeserver base_url is not a valid URL", () => { it("throws an error when homeserver base_url is not a valid URL", async () => {
const discoveryResult = { const discoveryResult = {
...validIsConfig, ...validIsConfig,
"m.homeserver": { "m.homeserver": {
@ -148,24 +158,25 @@ describe("AutoDiscoveryUtils", () => {
base_url: "banana", base_url: "banana",
}, },
}; };
expect(() => AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, discoveryResult)).toThrow( await expect(() =>
"Invalid URL: banana", AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, discoveryResult),
); ).rejects.toThrow("Invalid URL: banana");
}); });
it("uses hs url hostname when serverName is falsy in args and config", () => { it("uses hs url hostname when serverName is falsy in args and config", async () => {
const discoveryResult = { const discoveryResult = {
...validIsConfig, ...validIsConfig,
...validHsConfig, ...validHsConfig,
}; };
expect(AutoDiscoveryUtils.buildValidatedConfigFromDiscovery("", discoveryResult)).toEqual({ await expect(AutoDiscoveryUtils.buildValidatedConfigFromDiscovery("", discoveryResult)).resolves.toEqual({
...expectedValidatedConfig, ...expectedValidatedConfig,
hsNameIsDifferent: false, hsNameIsDifferent: false,
hsName: "matrix.org", hsName: "matrix.org",
warning: null,
}); });
}); });
it("uses serverName from props", () => { it("uses serverName from props", async () => {
const discoveryResult = { const discoveryResult = {
...validIsConfig, ...validIsConfig,
"m.homeserver": { "m.homeserver": {
@ -174,16 +185,17 @@ describe("AutoDiscoveryUtils", () => {
}, },
}; };
const syntaxOnly = true; const syntaxOnly = true;
expect( await expect(
AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, discoveryResult, syntaxOnly), AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, discoveryResult, syntaxOnly),
).toEqual({ ).resolves.toEqual({
...expectedValidatedConfig, ...expectedValidatedConfig,
hsNameIsDifferent: true, hsNameIsDifferent: true,
hsName: serverName, hsName: serverName,
warning: null,
}); });
}); });
it("ignores liveliness error when checking syntax only", () => { it("ignores liveliness error when checking syntax only", async () => {
const discoveryResult = { const discoveryResult = {
...validIsConfig, ...validIsConfig,
"m.homeserver": { "m.homeserver": {
@ -193,60 +205,15 @@ describe("AutoDiscoveryUtils", () => {
}, },
}; };
const syntaxOnly = true; const syntaxOnly = true;
expect( await expect(
AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, discoveryResult, syntaxOnly), AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, discoveryResult, syntaxOnly),
).toEqual({ ).resolves.toEqual({
...expectedValidatedConfig, ...expectedValidatedConfig,
warning: "Homeserver URL does not appear to be a valid Matrix homeserver", warning: "Homeserver URL does not appear to be a valid Matrix homeserver",
}); });
}); });
it("ignores delegated auth config when discovery was not successful", () => { it("handles homeserver too old error", async () => {
const discoveryResult = {
...validIsConfig,
...validHsConfig,
[M_AUTHENTICATION.stable!]: {
state: AutoDiscoveryAction.FAIL_ERROR,
error: "",
},
};
const syntaxOnly = true;
expect(
AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, discoveryResult, syntaxOnly),
).toEqual({
...expectedValidatedConfig,
delegatedAuthentication: undefined,
warning: undefined,
});
});
it("sets delegated auth config when discovery was successful", () => {
const authConfig = {
issuer: "https://test.com/",
authorizationEndpoint: "https://test.com/auth",
registrationEndpoint: "https://test.com/registration",
tokenEndpoint: "https://test.com/token",
};
const discoveryResult: ClientConfig = {
...validIsConfig,
...validHsConfig,
[M_AUTHENTICATION.stable!]: {
state: AutoDiscoveryAction.SUCCESS,
error: null,
...authConfig,
},
};
const syntaxOnly = true;
expect(
AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, discoveryResult, syntaxOnly),
).toEqual({
...expectedValidatedConfig,
delegatedAuthentication: authConfig,
warning: undefined,
});
});
it("handles homeserver too old error", () => {
const discoveryResult: ClientConfig = { const discoveryResult: ClientConfig = {
...validIsConfig, ...validIsConfig,
"m.homeserver": { "m.homeserver": {
@ -256,12 +223,165 @@ describe("AutoDiscoveryUtils", () => {
}, },
}; };
const syntaxOnly = true; const syntaxOnly = true;
expect(() => await expect(() =>
AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, discoveryResult, syntaxOnly), AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, discoveryResult, syntaxOnly),
).toThrow( ).rejects.toThrow(
"Your homeserver is too old and does not support the minimum API version required. Please contact your server owner, or upgrade your server.", "Your homeserver is too old and does not support the minimum API version required. Please contact your server owner, or upgrade your server.",
); );
}); });
it("should validate delegated oidc auth", async () => {
const issuer = "https://auth.matrix.org/";
fetchMock.get(
`${validHsConfig["m.homeserver"].base_url}/_matrix/client/unstable/org.matrix.msc2965/auth_issuer`,
{
issuer,
},
);
fetchMock.get(`${issuer}.well-known/openid-configuration`, {
...mockOpenIdConfiguration(issuer),
"scopes_supported": ["openid", "email"],
"response_modes_supported": ["form_post", "query", "fragment"],
"token_endpoint_auth_methods_supported": [
"client_secret_basic",
"client_secret_post",
"client_secret_jwt",
"private_key_jwt",
"none",
],
"token_endpoint_auth_signing_alg_values_supported": [
"HS256",
"HS384",
"HS512",
"RS256",
"RS384",
"RS512",
"PS256",
"PS384",
"PS512",
"ES256",
"ES384",
"ES256K",
],
"revocation_endpoint_auth_methods_supported": [
"client_secret_basic",
"client_secret_post",
"client_secret_jwt",
"private_key_jwt",
"none",
],
"revocation_endpoint_auth_signing_alg_values_supported": [
"HS256",
"HS384",
"HS512",
"RS256",
"RS384",
"RS512",
"PS256",
"PS384",
"PS512",
"ES256",
"ES384",
"ES256K",
],
"introspection_endpoint": `${issuer}oauth2/introspect`,
"introspection_endpoint_auth_methods_supported": [
"client_secret_basic",
"client_secret_post",
"client_secret_jwt",
"private_key_jwt",
"none",
],
"introspection_endpoint_auth_signing_alg_values_supported": [
"HS256",
"HS384",
"HS512",
"RS256",
"RS384",
"RS512",
"PS256",
"PS384",
"PS512",
"ES256",
"ES384",
"ES256K",
],
"userinfo_endpoint": `${issuer}oauth2/userinfo`,
"subject_types_supported": ["public"],
"id_token_signing_alg_values_supported": [
"RS256",
"RS384",
"RS512",
"ES256",
"ES384",
"PS256",
"PS384",
"PS512",
"ES256K",
],
"userinfo_signing_alg_values_supported": [
"RS256",
"RS384",
"RS512",
"ES256",
"ES384",
"PS256",
"PS384",
"PS512",
"ES256K",
],
"display_values_supported": ["page"],
"claim_types_supported": ["normal"],
"claims_supported": ["iss", "sub", "aud", "iat", "exp", "nonce", "auth_time", "at_hash", "c_hash"],
"claims_parameter_supported": false,
"request_parameter_supported": false,
"request_uri_parameter_supported": false,
"prompt_values_supported": ["none", "login", "create"],
"device_authorization_endpoint": `${issuer}oauth2/device`,
"org.matrix.matrix-authentication-service.graphql_endpoint": `${issuer}graphql`,
"account_management_uri": `${issuer}account/`,
"account_management_actions_supported": [
"org.matrix.profile",
"org.matrix.sessions_list",
"org.matrix.session_view",
"org.matrix.session_end",
"org.matrix.cross_signing_reset",
],
});
fetchMock.get(`${issuer}jwks`, {
keys: [],
});
const discoveryResult = {
...validIsConfig,
...validHsConfig,
};
await expect(
AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, discoveryResult),
).resolves.toEqual({
...expectedValidatedConfig,
hsNameIsDifferent: true,
hsName: serverName,
delegatedAuthentication: expect.objectContaining({
accountManagementActionsSupported: [
"org.matrix.profile",
"org.matrix.sessions_list",
"org.matrix.session_view",
"org.matrix.session_end",
"org.matrix.cross_signing_reset",
],
accountManagementEndpoint: "https://auth.matrix.org/account/",
authorizationEndpoint: "https://auth.matrix.org/auth",
metadata: expect.objectContaining({
issuer,
}),
registrationEndpoint: "https://auth.matrix.org/registration",
signingKeys: [],
tokenEndpoint: "https://auth.matrix.org/token",
}),
warning: null,
});
});
}); });
describe("authComponentStateForError", () => { describe("authComponentStateForError", () => {

View file

@ -46,8 +46,8 @@ describe("TokenRefresher", () => {
}; };
beforeEach(() => { beforeEach(() => {
fetchMock.get(`${authConfig.issuer}.well-known/openid-configuration`, authConfig.metadata); fetchMock.get(`${issuer}.well-known/openid-configuration`, authConfig.metadata);
fetchMock.get(`${authConfig.issuer}jwks`, { fetchMock.get(`${issuer}jwks`, {
status: 200, status: 200,
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@ -68,7 +68,7 @@ describe("TokenRefresher", () => {
const getPickleKey = jest.fn().mockResolvedValue(pickleKey); const getPickleKey = jest.fn().mockResolvedValue(pickleKey);
mockPlatformPeg({ getPickleKey }); mockPlatformPeg({ getPickleKey });
const refresher = new TokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims, userId); const refresher = new TokenRefresher(issuer, clientId, redirectUri, deviceId, idTokenClaims, userId);
await refresher.oidcClientReady; await refresher.oidcClientReady;
@ -83,7 +83,7 @@ describe("TokenRefresher", () => {
const getPickleKey = jest.fn().mockResolvedValue(null); const getPickleKey = jest.fn().mockResolvedValue(null);
mockPlatformPeg({ getPickleKey }); mockPlatformPeg({ getPickleKey });
const refresher = new TokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims, userId); const refresher = new TokenRefresher(issuer, clientId, redirectUri, deviceId, idTokenClaims, userId);
await refresher.oidcClientReady; await refresher.oidcClientReady;

View file

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

View file

@ -1,61 +0,0 @@
/*
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 { M_AUTHENTICATION } from "matrix-js-sdk/src/matrix";
import { getDelegatedAuthAccountUrl } from "../../../src/utils/oidc/getDelegatedAuthAccountUrl";
describe("getDelegatedAuthAccountUrl()", () => {
it("should return undefined when wk is undefined", () => {
expect(getDelegatedAuthAccountUrl(undefined)).toBeUndefined();
});
it("should return undefined when wk has no authentication config", () => {
expect(getDelegatedAuthAccountUrl({})).toBeUndefined();
});
it("should return undefined when wk authentication config has no configured account url", () => {
expect(
getDelegatedAuthAccountUrl({
[M_AUTHENTICATION.stable!]: {
issuer: "issuer.org",
},
}),
).toBeUndefined();
});
it("should return the account url for authentication config using the unstable prefix", () => {
expect(
getDelegatedAuthAccountUrl({
[M_AUTHENTICATION.unstable!]: {
issuer: "issuer.org",
account: "issuer.org/account",
},
}),
).toEqual("issuer.org/account");
});
it("should return the account url for authentication config using the stable prefix", () => {
expect(
getDelegatedAuthAccountUrl({
[M_AUTHENTICATION.stable!]: {
issuer: "issuer.org",
account: "issuer.org/account",
},
}),
).toEqual("issuer.org/account");
});
});

View file

@ -16,15 +16,15 @@ limitations under the License.
import fetchMockJest from "fetch-mock-jest"; import fetchMockJest from "fetch-mock-jest";
import { OidcError } from "matrix-js-sdk/src/oidc/error"; import { OidcError } from "matrix-js-sdk/src/oidc/error";
import { OidcClientConfig } from "matrix-js-sdk/src/matrix";
import { getOidcClientId } from "../../../src/utils/oidc/registerClient"; import { getOidcClientId } from "../../../src/utils/oidc/registerClient";
import { ValidatedDelegatedAuthConfig } from "../../../src/utils/ValidatedServerConfig";
import { mockPlatformPeg } from "../../test-utils"; import { mockPlatformPeg } from "../../test-utils";
import PlatformPeg from "../../../src/PlatformPeg"; import PlatformPeg from "../../../src/PlatformPeg";
import { makeDelegatedAuthConfig } from "../../test-utils/oidc";
describe("getOidcClientId()", () => { describe("getOidcClientId()", () => {
const issuer = "https://auth.com/"; const issuer = "https://auth.com/";
const registrationEndpoint = "https://auth.com/register";
const clientName = "Element"; const clientName = "Element";
const baseUrl = "https://just.testing"; const baseUrl = "https://just.testing";
const dynamicClientId = "xyz789"; const dynamicClientId = "xyz789";
@ -33,12 +33,7 @@ describe("getOidcClientId()", () => {
client_id: "abc123", client_id: "abc123",
}, },
}; };
const delegatedAuthConfig = { const delegatedAuthConfig = makeDelegatedAuthConfig(issuer);
issuer,
registrationEndpoint,
authorizationEndpoint: issuer + "auth",
tokenEndpoint: issuer + "token",
};
beforeEach(() => { beforeEach(() => {
fetchMockJest.mockClear(); fetchMockJest.mockClear();
@ -63,11 +58,10 @@ describe("getOidcClientId()", () => {
}); });
it("should throw when no static clientId is configured and no registration endpoint", async () => { it("should throw when no static clientId is configured and no registration endpoint", async () => {
const authConfigWithoutRegistration: ValidatedDelegatedAuthConfig = { const authConfigWithoutRegistration: OidcClientConfig = makeDelegatedAuthConfig(
...delegatedAuthConfig, "https://issuerWithoutStaticClientId.org/",
issuer: "https://issuerWithoutStaticClientId.org/", );
registrationEndpoint: undefined, authConfigWithoutRegistration.registrationEndpoint = undefined;
};
await expect(getOidcClientId(authConfigWithoutRegistration, staticOidcClients)).rejects.toThrow( await expect(getOidcClientId(authConfigWithoutRegistration, staticOidcClients)).rejects.toThrow(
OidcError.DynamicRegistrationNotSupported, OidcError.DynamicRegistrationNotSupported,
); );
@ -76,7 +70,7 @@ describe("getOidcClientId()", () => {
}); });
it("should handle when staticOidcClients object is falsy", async () => { it("should handle when staticOidcClients object is falsy", async () => {
const authConfigWithoutRegistration: ValidatedDelegatedAuthConfig = { const authConfigWithoutRegistration: OidcClientConfig = {
...delegatedAuthConfig, ...delegatedAuthConfig,
registrationEndpoint: undefined, registrationEndpoint: undefined,
}; };
@ -88,14 +82,14 @@ describe("getOidcClientId()", () => {
}); });
it("should make correct request to register client", async () => { it("should make correct request to register client", async () => {
fetchMockJest.post(registrationEndpoint, { fetchMockJest.post(delegatedAuthConfig.registrationEndpoint!, {
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(
registrationEndpoint, delegatedAuthConfig.registrationEndpoint!,
expect.objectContaining({ expect.objectContaining({
headers: { headers: {
"Accept": "application/json", "Accept": "application/json",
@ -120,14 +114,14 @@ describe("getOidcClientId()", () => {
}); });
it("should throw when registration request fails", async () => { it("should throw when registration request fails", async () => {
fetchMockJest.post(registrationEndpoint, { fetchMockJest.post(delegatedAuthConfig.registrationEndpoint!, {
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(registrationEndpoint, { fetchMockJest.post(delegatedAuthConfig.registrationEndpoint!, {
status: 200, status: 200,
// no clientId in response // no clientId in response
body: "{}", body: "{}",