Adapt OAuth2 implementation to Matrix Spec v1.18 (#34026)

* Adapt OAuth2 implementation to Matrix Spec v1.18

* Handle more cases of oidc->oauth

* Fix test

* Fix read back of oauth2 context

* Iterate

* Fix tests

* Discard changes to apps/web/playwright/e2e/settings/account-user-settings-tab.spec.ts

* Fix test

* Fix test

* Fix test

* Potential fix for pull request finding 'Unused variable, import, function or class'

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>

* Iterate

* Iterate

* Fix test

---------

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
This commit is contained in:
Michael Telatynski
2026-07-08 08:02:25 +00:00
committed by GitHub
co-authored by Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
parent 38e29c51c4
commit 2bc9656957
69 changed files with 761 additions and 1738 deletions
@@ -6,4 +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.
*/
export { makeDelegatedAuthConfig, mockOpenIdConfiguration } from "matrix-js-sdk/src/testing";
export { makeDelegatedAuthMetadata } from "matrix-js-sdk/src/testing";
+2 -2
View File
@@ -27,7 +27,7 @@ import {
type IPushRules,
RelationType,
JoinRule,
type OidcClientConfig,
type ValidatedAuthMetadata,
type GroupCall,
type EventStatus,
type ICreateRoomOpts,
@@ -778,7 +778,7 @@ export function mkRoomState(
export function mkServerConfig(
hsUrl: string,
isUrl: string,
delegatedAuthentication?: OidcClientConfig,
delegatedAuthentication?: ValidatedAuthMetadata,
): ValidatedServerConfig {
return {
hsUrl,
@@ -57,6 +57,7 @@ describe("<LoggedInView />", () => {
setExtendedProfileProperty: jest.fn().mockResolvedValue(undefined),
deleteExtendedProfileProperty: jest.fn().mockResolvedValue(undefined),
doesServerSupportExtendedProfiles: jest.fn().mockResolvedValue(true),
getAuthMetadata: jest.fn().mockRejectedValue(new Error("Legacy auth")),
});
const mediaHandler = new MediaHandler(mockClient);
const mockSdkContext = new TestSDKContext();
@@ -9,13 +9,19 @@ Please see LICENSE files in the repository root for full details.
import React, { type ComponentProps, createRef, type RefObject } from "react";
import { fireEvent, render, type RenderResult, screen, waitFor, within, act } from "jest-matrix-react";
import { type Mocked, mocked } from "jest-mock-vitest-adapter";
import { ClientEvent, type MatrixClient, MatrixEvent, Room, SyncState } from "matrix-js-sdk/src/matrix";
import {
ClientEvent,
type MatrixClient,
MatrixEvent,
Room,
SyncState,
OAuth2,
type BearerTokenResponse,
OAuth2Error,
} from "matrix-js-sdk/src/matrix";
import { type MediaHandler } from "matrix-js-sdk/src/webrtc/mediaHandler";
import * as MatrixJs from "matrix-js-sdk/src/matrix";
import { completeAuthorizationCodeGrant } from "matrix-js-sdk/src/oidc/authorize";
import { logger } from "matrix-js-sdk/src/logger";
import { OidcError } from "matrix-js-sdk/src/oidc/error";
import { type BearerTokenResponse } from "matrix-js-sdk/src/oidc/validate";
import { sleep } from "matrix-js-sdk/src/utils";
import {
CryptoEvent,
@@ -45,7 +51,7 @@ import {
unmockClientPeg,
} from "../../../test-utils";
import * as leaveRoomUtils from "../../../../src/utils/leave-behaviour";
import { OidcClientError } from "../../../../src/utils/oidc/error";
import { OAuthClientError } from "../../../../src/utils/oauth/error";
import { CallStore } from "../../../../src/stores/CallStore";
import { type Call } from "../../../../src/models/Call";
import { PosthogAnalytics } from "../../../../src/PosthogAnalytics";
@@ -68,12 +74,9 @@ import { ShareFormat } from "../../../../src/dispatcher/payloads/SharePayload.ts
import { clearStorage } from "../../../../src/Lifecycle";
import UserSettingsDialog from "../../../../src/components/views/dialogs/UserSettingsDialog.tsx";
import { SDKContextClass } from "../../../../src/contexts/SDKContextClass";
import { makeDelegatedAuthConfig } from "../../../test-utils/oidc.ts";
import { makeDelegatedAuthMetadata } from "../../../test-utils/auth.ts";
import { type QrLoginCredentials } from "../../../../src/components/views/auth/LoginWithQR.tsx";
jest.mock("matrix-js-sdk/src/oidc/authorize", () => ({
completeAuthorizationCodeGrant: jest.fn(),
}));
import { storeAuthContext } from "../../../../src/utils/oauth/persistOAuthSettings.ts";
// Stub out ThemeWatcher as the necessary bits for themes are done in element-web's index.html and thus are lacking here,
// plus JSDOM's implementation of CSSStyleDeclaration has a bunch of differences to real browsers which cause issues.
@@ -193,6 +196,7 @@ describe("<MatrixChat />", () => {
logout: jest.fn(),
getDeviceId: jest.fn(),
forget: () => Promise.resolve(),
getAuthMetadata: jest.fn().mockRejectedValue(new Error("Legacy auth")),
});
let mockClient: Mocked<MatrixClient>;
const serverConfig = {
@@ -268,6 +272,10 @@ describe("<MatrixChat />", () => {
bootstrapDeferred = Promise.withResolvers();
await clearAllModals();
jest.spyOn(OAuth2.prototype, "completeAuthorizationCodeGrant").mockImplementation(
(code) => new Promise<BearerTokenResponse>(() => {}),
);
});
afterEach(async () => {
@@ -341,16 +349,16 @@ describe("<MatrixChat />", () => {
describe("qr login", () => {
beforeEach(() => {
const authConfig = makeDelegatedAuthConfig();
const authConfig = makeDelegatedAuthMetadata();
defaultProps.config.validated_server_config!.delegatedAuthentication = authConfig;
fetchMock.post(authConfig.registration_endpoint!, { client_id: "abc123" });
mockPlatformPeg({
getOidcClientMetadata: jest.fn().mockReturnValue({
clientName: "App name",
clientUri: "https://company",
redirectUris: ["https://app"],
logoUri: "https://company/logo.png",
applicationType: "web",
getOAuthClientMetadata: jest.fn().mockReturnValue({
client_name: "App name",
client_uri: "https://company",
redirect_uris: ["https://app"],
logo_uri: "https://company/logo.png",
application_type: "web",
}),
});
jest.spyOn(qrLogin, "signInByGeneratingQR").mockReturnValue(new Promise(() => {}));
@@ -384,8 +392,6 @@ describe("<MatrixChat />", () => {
accessToken: "at",
homeserverUrl: "https://homeserver",
clientId: "ci",
idToken: "it",
issuer: defaultProps.config.validated_server_config!.delegatedAuthentication!.issuer,
deviceId: "di",
secrets: {
cross_signing: {
@@ -452,7 +458,6 @@ describe("<MatrixChat />", () => {
});
describe("when query params have a OIDC params", () => {
const issuer = "https://auth.com/";
const homeserverUrl = "https://matrix.org";
const identityServerUrl = "https://is.org";
const clientId = "xyz789";
@@ -460,7 +465,7 @@ describe("<MatrixChat />", () => {
const code = "test-oidc-auth-code";
const state = "test-oidc-state";
const urlParams = {
oidc_fragment: {
oauth2: {
code,
state: state,
},
@@ -472,15 +477,14 @@ describe("<MatrixChat />", () => {
const tokenResponse: BearerTokenResponse = {
access_token: accessToken,
refresh_token: undefined,
id_token: "ghi789",
scope: "test",
token_type: "Bearer",
expires_at: 12345,
expires_in: 12345,
};
let loginClient!: ReturnType<typeof getMockClientWithEventEmitter>;
const expectOIDCError = async (
const expectOAuthError = async (
errorMessage = "Something went wrong during authentication. Go to the sign in page and try again.",
): Promise<void> => {
await flushPromises();
@@ -490,24 +494,7 @@ describe("<MatrixChat />", () => {
};
beforeEach(() => {
mocked(completeAuthorizationCodeGrant)
.mockClear()
.mockResolvedValue({
oidcClientSettings: {
clientId,
issuer,
},
tokenResponse,
homeserverUrl,
identityServerUrl,
idTokenClaims: {
aud: "123",
iss: issuer,
sub: "123",
exp: 123,
iat: 456,
},
});
mocked(OAuth2.prototype.completeAuthorizationCodeGrant).mockResolvedValue(tokenResponse);
loginClient = getMockClientWithEventEmitter(getMockClientMethods());
// this is used to create a temporary client during login
@@ -521,11 +508,23 @@ describe("<MatrixChat />", () => {
device_id: deviceId,
is_guest: false,
});
storeAuthContext({
homeserverUrl,
identityServerUrl,
metadata: makeDelegatedAuthMetadata(),
state,
authContext: {
clientId,
codeVerifier: "123456",
deviceId,
redirectUri: "https://cb",
},
});
});
it("should fail when query params do not include valid code and state", async () => {
it("should fail when fragment params do not include valid code and state", async () => {
const urlParams = {
oidc_query: {
oauth2: {
code: "",
state: "abc",
},
@@ -535,11 +534,11 @@ describe("<MatrixChat />", () => {
await flushPromises();
expect(logger.error).toHaveBeenCalledWith(
"Failed to login via OIDC",
new Error(OidcClientError.InvalidQueryParameters),
"Failed to login via OAuth",
new Error(OAuthClientError.InvalidFragmentParameters),
);
await expectOIDCError();
await expectOAuthError();
});
it("should make correct request to complete authorization", async () => {
@@ -547,7 +546,7 @@ describe("<MatrixChat />", () => {
await flushPromises();
expect(completeAuthorizationCodeGrant).toHaveBeenCalledWith(code, state, "fragment");
expect(OAuth2.prototype.completeAuthorizationCodeGrant).toHaveBeenCalledWith(code);
});
it("should look up userId using access token", async () => {
@@ -571,10 +570,10 @@ describe("<MatrixChat />", () => {
await flushPromises();
expect(logger.error).toHaveBeenCalledWith(
"Failed to login via OIDC",
"Failed to login via OAuth",
new Error("Failed to retrieve userId using accessToken"),
);
await expectOIDCError();
await expectOAuthError();
});
it("should call onTokenLoginCompleted", async () => {
@@ -586,23 +585,26 @@ describe("<MatrixChat />", () => {
describe("when login fails", () => {
beforeEach(() => {
mocked(completeAuthorizationCodeGrant).mockRejectedValue(new Error(OidcError.CodeExchangeFailed));
mocked(OAuth2.prototype.completeAuthorizationCodeGrant).mockRejectedValue(
new Error(OAuth2Error.CodeExchangeFailed),
);
});
it("should log and return to welcome page with correct error when login state is not found", async () => {
mocked(completeAuthorizationCodeGrant).mockRejectedValue(
new Error(OidcError.MissingOrInvalidStoredState),
sessionStorage.clear();
mocked(OAuth2.prototype.completeAuthorizationCodeGrant).mockRejectedValue(
new Error(OAuth2Error.MissingOrInvalidStoredState),
);
getComponent({ urlParams });
await flushPromises();
expect(logger.error).toHaveBeenCalledWith(
"Failed to login via OIDC",
new Error(OidcError.MissingOrInvalidStoredState),
"Failed to login via OAuth",
new Error(OAuth2Error.MissingOrInvalidStoredState),
);
await expectOIDCError(
await expectOAuthError(
"We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.",
);
});
@@ -613,12 +615,12 @@ describe("<MatrixChat />", () => {
await flushPromises();
expect(logger.error).toHaveBeenCalledWith(
"Failed to login via OIDC",
new Error(OidcError.CodeExchangeFailed),
"Failed to login via OAuth",
new Error(OAuth2Error.CodeExchangeFailed),
);
// warning dialog
await expectOIDCError();
await expectOAuthError();
});
it("should not clear storage", async () => {
@@ -636,7 +638,6 @@ describe("<MatrixChat />", () => {
await flushPromises();
expect(sessionStorageSetSpy).not.toHaveBeenCalledWith("mx_oidc_client_id", clientId);
expect(sessionStorageSetSpy).not.toHaveBeenCalledWith("mx_oidc_token_issuer", issuer);
});
});
@@ -658,7 +659,6 @@ describe("<MatrixChat />", () => {
getComponent({ urlParams });
await waitFor(() => expect(localStorage.getItem("mx_oidc_client_id")).toEqual(clientId));
await waitFor(() => expect(localStorage.getItem("mx_oidc_token_issuer")).toEqual(issuer));
});
it("should set logged in and start MatrixClient", async () => {
@@ -9,22 +9,26 @@ import React from "react";
import { fireEvent, render, screen, waitForElementToBeRemoved } from "jest-matrix-react";
import { mocked, type MockedObject } from "jest-mock-vitest-adapter";
import fetchMock from "@fetch-mock/jest";
import { DELEGATED_OIDC_COMPATIBILITY, IdentityProviderBrand, type OidcClientConfig } from "matrix-js-sdk/src/matrix";
import {
OAUTH_AWARE_PREFERRED_FLOW_FIELD,
IdentityProviderBrand,
type ValidatedAuthMetadata,
} from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger";
import * as Matrix from "matrix-js-sdk/src/matrix";
import { OidcError } from "matrix-js-sdk/src/oidc/error";
import { OAuth2Error } from "matrix-js-sdk/src/matrix";
import SdkConfig from "../../../../../src/SdkConfig";
import { mkServerConfig, mockPlatformPeg, unmockPlatformPeg } from "../../../../test-utils";
import Login from "../../../../../src/components/structures/auth/Login";
import type BasePlatform from "../../../../../src/BasePlatform";
import * as registerClientUtils from "../../../../../src/utils/oidc/registerClient";
import { makeDelegatedAuthConfig } from "../../../../test-utils/oidc";
import * as registerClientUtils from "../../../../../src/utils/oauth/registerClient";
import { makeDelegatedAuthMetadata } from "../../../../test-utils/auth";
import { ModuleApi } from "../../../../../src/modules/Api.ts";
jest.useRealTimers();
const oidcStaticClientsConfig = {
const oauthStaticClientsConfig = {
"https://staticallyregisteredissuer.org/": {
client_id: "static-clientId-123",
},
@@ -42,7 +46,7 @@ describe("Login", function () {
SdkConfig.put({
brand: "test-brand",
disable_custom_urls: true,
oidc_static_clients: oidcStaticClientsConfig,
oidc_static_clients: oauthStaticClientsConfig,
});
mockClient.login.mockClear().mockResolvedValue({
access_token: "TOKEN",
@@ -72,7 +76,7 @@ describe("Login", function () {
function getRawComponent(
hsUrl = "https://matrix.org",
isUrl = "https://vector.im",
delegatedAuthentication?: OidcClientConfig,
delegatedAuthentication?: ValidatedAuthMetadata,
) {
return (
<Login
@@ -84,7 +88,7 @@ describe("Login", function () {
);
}
function getComponent(hsUrl?: string, isUrl?: string, delegatedAuthentication?: OidcClientConfig) {
function getComponent(hsUrl?: string, isUrl?: string, delegatedAuthentication?: ValidatedAuthMetadata) {
return render(getRawComponent(hsUrl, isUrl, delegatedAuthentication));
}
@@ -269,7 +273,7 @@ describe("Login", function () {
flows: [
{
type: "m.login.sso",
[DELEGATED_OIDC_COMPATIBILITY.name]: true,
[OAUTH_AWARE_PREFERRED_FLOW_FIELD.name]: true,
},
{
type: "m.login.password",
@@ -393,7 +397,7 @@ describe("Login", function () {
const hsUrl = "https://matrix.org";
const isUrl = "https://vector.im";
const issuer = "https://test.com/";
const delegatedAuth = makeDelegatedAuthConfig(issuer);
const delegatedAuth = makeDelegatedAuthMetadata(issuer);
beforeEach(() => {
jest.spyOn(logger, "error");
});
@@ -402,9 +406,9 @@ describe("Login", function () {
jest.spyOn(logger, "error").mockRestore();
});
it("should attempt to register oidc client", async () => {
it("should attempt to register oauth client", async () => {
// dont mock, spy so we can check config values were correctly passed
jest.spyOn(registerClientUtils, "getOidcClientId");
jest.spyOn(registerClientUtils, "getOAuthClientId");
fetchMock.post(delegatedAuth.registration_endpoint!, { status: 500 });
getComponent(hsUrl, isUrl, delegatedAuth);
@@ -413,7 +417,7 @@ describe("Login", function () {
// tried to register
expect(fetchMock).toHaveFetched(delegatedAuth.registration_endpoint);
// called with values from config
expect(registerClientUtils.getOidcClientId).toHaveBeenCalledWith(delegatedAuth, oidcStaticClientsConfig);
expect(registerClientUtils.getOAuthClientId).toHaveBeenCalledWith(delegatedAuth, oauthStaticClientsConfig);
});
it("should fallback to normal login when client registration fails", async () => {
@@ -425,8 +429,8 @@ describe("Login", function () {
// tried to register
expect(fetchMock).toHaveFetched(delegatedAuth.registration_endpoint);
expect(logger.error).toHaveBeenCalledWith(
"Failed to get oidc native flow",
new Error(OidcError.DynamicRegistrationFailed),
"Failed to get OAuth2 native flow",
new Error(OAuth2Error.DynamicRegistrationFailed),
);
// continued with normal setup
@@ -436,7 +440,7 @@ describe("Login", function () {
});
// 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 oauth native flow is correctly configured", async () => {
fetchMock.post(delegatedAuth.registration_endpoint!, { client_id: "abc123" });
getComponent(hsUrl, isUrl, delegatedAuth);
@@ -9,7 +9,7 @@ Please see LICENSE files in the repository root for full details.
import React from "react";
import { fireEvent, render, screen, waitFor, waitForElementToBeRemoved } from "jest-matrix-react";
import { createClient, type MatrixClient, MatrixError, type OidcClientConfig } from "matrix-js-sdk/src/matrix";
import { createClient, type MatrixClient, MatrixError, type ValidatedAuthMetadata } from "matrix-js-sdk/src/matrix";
import { mocked, type MockedObject } from "jest-mock-vitest-adapter";
import fetchMock from "@fetch-mock/jest";
@@ -21,11 +21,11 @@ import {
unmockPlatformPeg,
} from "../../../../test-utils";
import Registration from "../../../../../src/components/structures/auth/Registration";
import { makeDelegatedAuthConfig } from "../../../../test-utils/oidc";
import { startOidcLogin } from "../../../../../src/utils/oidc/authorize";
import { makeDelegatedAuthMetadata } from "../../../../test-utils/auth";
import { startOAuthLogin } from "../../../../../src/utils/oauth/authorize";
jest.mock("../../../../../src/utils/oidc/authorize", () => ({
startOidcLogin: jest.fn(),
jest.mock("../../../../../src/utils/oauth/authorize", () => ({
startOAuthLogin: jest.fn(),
}));
jest.mock("matrix-js-sdk/src/matrix", () => ({
@@ -92,7 +92,7 @@ describe("Registration", function () {
function getRawComponent(
hsUrl = defaultHsUrl,
isUrl = defaultIsUrl,
authConfig?: OidcClientConfig,
authConfig?: ValidatedAuthMetadata,
mobileRegister?: boolean,
) {
return (
@@ -104,7 +104,12 @@ describe("Registration", function () {
);
}
function getComponent(hsUrl?: string, isUrl?: string, authConfig?: OidcClientConfig, mobileRegister?: boolean) {
function getComponent(
hsUrl?: string,
isUrl?: string,
authConfig?: ValidatedAuthMetadata,
mobileRegister?: boolean,
) {
return render(getRawComponent(hsUrl, isUrl, authConfig, mobileRegister));
}
@@ -155,7 +160,7 @@ describe("Registration", function () {
});
describe("when delegated authentication is configured and enabled", () => {
const authConfig = makeDelegatedAuthConfig();
const authConfig = makeDelegatedAuthMetadata();
const clientId = "test-client-id";
authConfig.prompt_values_supported = ["create"];
@@ -168,15 +173,6 @@ describe("Registration", function () {
},
},
});
fetchMock.get(`${defaultHsUrl}/_matrix/client/unstable/org.matrix.msc2965/auth_issuer`, {
issuer: authConfig.issuer,
});
fetchMock.get("https://auth.org/.well-known/openid-configuration", {
...authConfig,
signingKeys: undefined,
});
fetchMock.get(authConfig.jwks_uri!, { keys: [] });
});
it("should display oidc-native continue button", async () => {
@@ -194,7 +190,7 @@ describe("Registration", function () {
fireEvent.click(await screen.findByText("Continue"));
expect(startOidcLogin).toHaveBeenCalledWith(
expect(startOAuthLogin).toHaveBeenCalledWith(
authConfig,
clientId,
defaultHsUrl,
@@ -28,7 +28,7 @@ import { UIFeature } from "../../../../../src/settings/UIFeature";
import { SettingLevel } from "../../../../../src/settings/SettingLevel";
import { TestSDKContext } from "../../../TestSDKContext.ts";
import { type FeatureSettingKey } from "../../../../../src/settings/Settings.tsx";
import { mockOpenIdConfiguration } from "../../../../test-utils/oidc.ts";
import { makeDelegatedAuthMetadata } from "../../../../test-utils/auth.ts";
mockPlatformPeg({
supportsSpellCheckSettings: jest.fn().mockReturnValue(false),
@@ -74,7 +74,7 @@ describe("<UserSettingsDialog />", () => {
getPushers: jest.fn().mockResolvedValue([]),
getProfileInfo: jest.fn().mockResolvedValue({}),
getMediaConfig: jest.fn(),
getAuthMetadata: jest.fn().mockResolvedValue(mockOpenIdConfiguration()),
getAuthMetadata: jest.fn().mockResolvedValue(makeDelegatedAuthMetadata()),
});
sdkContext = new TestSDKContext();
sdkContext._client = mockClient;
@@ -16,7 +16,7 @@ import {
RendezvousError,
RendezvousIntent,
} from "matrix-js-sdk/src/rendezvous";
import { mockOpenIdConfiguration } from "matrix-js-sdk/src/testing";
import { makeDelegatedAuthMetadata } from "matrix-js-sdk/src/testing";
import {
AutoDiscovery,
AutoDiscoveryAction,
@@ -61,7 +61,7 @@ function makeClient() {
getClientWellKnown: jest.fn().mockReturnValue({}),
getCrypto: jest.fn().mockReturnValue({}),
getDomain: jest.fn(),
getAuthMetadata: jest.fn().mockReturnValue(mockOpenIdConfiguration()),
getAuthMetadata: jest.fn().mockReturnValue(makeDelegatedAuthMetadata()),
} as unknown as MatrixClient);
cli.http = new MatrixHttpApi(cli, {
@@ -330,24 +330,23 @@ describe("<LoginWithQR />", () => {
test("should handle qr login", async () => {
fetchMock.get("https://hs/_matrix/client/versions", {
unstable_features: {},
versions: ["v1.1", "v1.5", "v1.6", "v1.8", "v1.9"],
versions: ["v1.1", "v1.5", "v1.6", "v1.8", "v1.9", "v1.15"],
});
const authMetadata = {
...mockOpenIdConfiguration("https://auth.org/", [OAuthGrantType.DeviceAuthorization]),
jwks_uri: undefined,
};
fetchMock.get("https://hs/_matrix/client/unstable/org.matrix.msc2965/auth_metadata", authMetadata);
const authMetadata = makeDelegatedAuthMetadata("https://auth.org/", [
OAuthGrantType.DeviceAuthorization,
]);
fetchMock.get("https://hs/_matrix/client/v1/auth_metadata", authMetadata);
fetchMock.post(authMetadata.registration_endpoint!, {
client_id: "!client_id!",
});
mockPlatformPeg({
getOidcClientMetadata: jest.fn().mockReturnValue({
clientName: "App name",
clientUri: "https://company",
redirectUris: ["https://app"],
logoUri: "https://company/logo.png",
applicationType: "web",
getOAuthClientMetadata: jest.fn().mockReturnValue({
client_name: "App name",
client_uri: "https://company",
redirect_uris: ["https://app"],
logo_uri: "https://company/logo.png",
application_type: "web",
}),
});
@@ -26,7 +26,6 @@ import {
flushPromises,
} from "../../../../../../test-utils";
import { UIFeature } from "../../../../../../../src/settings/UIFeature";
import { type OidcClientStore } from "../../../../../../../src/stores/oidc/OidcClientStore";
import MatrixClientContext from "../../../../../../../src/contexts/MatrixClientContext";
import Modal from "../../../../../../../src/Modal";
@@ -77,6 +76,8 @@ describe("<AccountUserSettingsTab />", () => {
getThreePids: jest.fn(),
getIdentityServerUrl: jest.fn(),
deleteThreePid: jest.fn(),
getMediaConfig: jest.fn(),
getAuthMetadata: jest.fn().mockRejectedValue(new Error("not implemented")),
});
mockClient.getCapabilities.mockResolvedValue({});
@@ -89,9 +90,6 @@ describe("<AccountUserSettingsTab />", () => {
stores = new TestSDKContext();
stores._client = mockClient;
// stub out this store completely to avoid mocking initialisation
const mockOidcClientStore = {} as unknown as OidcClientStore;
jest.spyOn(stores, "oidcClientStore", "get").mockReturnValue(mockOidcClientStore);
});
afterEach(() => {
@@ -107,10 +105,9 @@ describe("<AccountUserSettingsTab />", () => {
it("show account management link in expected format", async () => {
const accountManagementLink = "https://id.server.org/my-account";
const mockOidcClientStore = {
accountManagementEndpoint: accountManagementLink,
} as unknown as OidcClientStore;
jest.spyOn(stores, "oidcClientStore", "get").mockReturnValue(mockOidcClientStore);
mockClient.getAuthMetadata.mockResolvedValue({
account_management_uri: accountManagementLink,
} as any);
render(getComponent());
@@ -134,10 +131,9 @@ describe("<AccountUserSettingsTab />", () => {
);
// account is managed externally when we have delegated auth configured
const accountManagementLink = "https://id.server.org/my-account";
const mockOidcClientStore = {
accountManagementEndpoint: accountManagementLink,
} as unknown as OidcClientStore;
jest.spyOn(stores, "oidcClientStore", "get").mockReturnValue(mockOidcClientStore);
mockClient.getAuthMetadata.mockResolvedValue({
account_management_uri: accountManagementLink,
} as any);
render(getComponent());
await flushPromises();
@@ -207,11 +203,6 @@ describe("<AccountUserSettingsTab />", () => {
describe("3pids", () => {
beforeEach(() => {
const mockOidcClientStore = {
accountManagementEndpoint: undefined,
} as unknown as OidcClientStore;
jest.spyOn(stores, "oidcClientStore", "get").mockReturnValue(mockOidcClientStore);
mockClient.getCapabilities.mockResolvedValue({
"m.3pid_changes": {
enabled: true,
@@ -34,7 +34,6 @@ import {
type MatrixClient,
} from "matrix-js-sdk/src/matrix";
import { mocked, type MockedObject } from "jest-mock-vitest-adapter";
import fetchMock from "@fetch-mock/jest";
import {
clearAllModals,
@@ -58,8 +57,7 @@ import SettingsStore from "../../../../../../../src/settings/SettingsStore";
import { getClientInformationEventType } from "../../../../../../../src/utils/device/clientInformation";
import { SDKContext } from "../../../../../../../src/contexts/SDKContext";
import { TestSDKContext } from "../../../../../TestSDKContext.ts";
import { type OidcClientStore } from "../../../../../../../src/stores/oidc/OidcClientStore";
import { makeDelegatedAuthConfig } from "../../../../../../test-utils/oidc";
import { makeDelegatedAuthMetadata } from "../../../../../../test-utils/auth";
import MatrixClientContext from "../../../../../../../src/contexts/MatrixClientContext";
mockPlatformPeg();
@@ -1180,10 +1178,10 @@ describe("<SessionManagerTab />", () => {
describe("for an OIDC-aware server", () => {
beforeEach(() => {
// just do an ugly mock here to avoid mocking initialisation
const mockOidcClientStore = {
accountManagementEndpoint: "https://issuer.org/account",
} as unknown as OidcClientStore;
jest.spyOn(sdkContext, "oidcClientStore", "get").mockReturnValue(mockOidcClientStore);
mockClient.getAuthMetadata.mockResolvedValue({
...makeDelegatedAuthMetadata(),
account_management_uri: "https://issuer.org/account",
} as any);
});
// signing out the current device works as usual
@@ -1637,7 +1635,7 @@ describe("<SessionManagerTab />", () => {
enabled: true,
},
});
const delegatedAuthConfig = makeDelegatedAuthConfig(issuer);
const delegatedAuthConfig = makeDelegatedAuthMetadata(issuer);
mockClient.getAuthMetadata.mockResolvedValue({
...delegatedAuthConfig,
grant_types_supported: [
@@ -1646,13 +1644,6 @@ describe("<SessionManagerTab />", () => {
],
});
mockCrypto.exportSecretsBundle = jest.fn();
fetchMock.route(delegatedAuthConfig.jwks_uri!, {
status: 200,
headers: {
"Content-Type": "application/json",
},
keys: [],
});
});
it("renders qr code login section", async () => {
@@ -123,6 +123,7 @@ describe("<SpacePanel />", () => {
removeListener: jest.fn(),
isVersionSupported: jest.fn().mockResolvedValue(true),
doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(false),
getAuthMetadata: jest.fn().mockRejectedValue(new Error("Legacy auth")),
} as unknown as MatrixClient;
const sdkContext = new TestSDKContext();
const SpacePanel = wrapInSdkContext(wrapInMatrixClientContext(UnwrappedSpacePanel), sdkContext);
@@ -9,7 +9,6 @@ Please see LICENSE files in the repository root for full details.
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
import { SDKContextClass } from "../../../src/contexts/SDKContextClass";
import { OidcClientStore } from "../../../src/stores/oidc/OidcClientStore";
import { UserProfilesStore } from "../../../src/stores/UserProfilesStore";
import { createTestClient } from "../../test-utils";
import { TestSDKContext } from "../TestSDKContext.ts";
@@ -35,10 +34,6 @@ describe("SDKContextClass", () => {
expect(() => sdkContext.userProfilesStore).toThrow("Unable to create UserProfilesStore without a client");
});
it("oidcClientStore should raise an error without a client", () => {
expect(() => sdkContext.oidcClientStore).toThrow("Unable to create OidcClientStore without a client");
});
describe("when SDKContext has a client", () => {
beforeEach(() => {
sdkContext._client = client;
@@ -57,12 +52,5 @@ describe("SDKContextClass", () => {
sdkContext._client = client;
expect(sdkContext.userProfilesStore).not.toBe(store);
});
it("oidcClientstore should return a OidcClientStore", () => {
const store = sdkContext.oidcClientStore;
expect(store).toBeInstanceOf(OidcClientStore);
// it should return the same instance
expect(sdkContext.oidcClientStore).toBe(store);
});
});
});
@@ -1,260 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
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/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 { makeDelegatedAuthConfig } from "../../../test-utils/oidc";
jest.mock("matrix-js-sdk/src/matrix", () => ({
...jest.requireActual("matrix-js-sdk/src/matrix"),
discoverAndValidateOIDCIssuerWellKnown: jest.fn(),
}));
describe("OidcClientStore", () => {
const clientId = "test-client-id";
const authConfig = makeDelegatedAuthConfig();
const account = authConfig.issuer + "account";
const accountManagementActionsSupported = ["action1", "action2"];
const mockClient = getMockClientWithEventEmitter({
getAuthMetadata: jest.fn(),
});
beforeEach(() => {
localStorage.clear();
localStorage.setItem("mx_oidc_client_id", clientId);
localStorage.setItem("mx_oidc_token_issuer", authConfig.issuer);
mocked(discoverAndValidateOIDCIssuerWellKnown)
.mockClear()
.mockResolvedValue({
...authConfig,
account_management_uri: account,
account_management_actions_supported: accountManagementActionsSupported,
authorization_endpoint: "authorization-endpoint",
token_endpoint: "token-endpoint",
});
jest.spyOn(logger, "error").mockClear();
fetchMock.get(`${authConfig.issuer}.well-known/openid-configuration`, authConfig);
fetchMock.get(`${authConfig.issuer}jwks`, { keys: [] });
mockPlatformPeg();
});
describe("isUserAuthenticatedWithOidc()", () => {
it("should return true when an issuer is in session storage", () => {
const store = new OidcClientStore(mockClient);
expect(store.isUserAuthenticatedWithOidc).toEqual(true);
});
it("should return false when no issuer is in session storage", () => {
localStorage.clear();
const store = new OidcClientStore(mockClient);
expect(store.isUserAuthenticatedWithOidc).toEqual(false);
});
});
describe("initialising oidcClient", () => {
it("should initialise oidc client from constructor", () => {
const store = new OidcClientStore(mockClient);
// started initialising
// @ts-ignore private property
expect(store.initialisingOidcClientPromise).toBeTruthy();
});
it("should fallback to stored issuer when no client well known is available", async () => {
const store = new OidcClientStore(mockClient);
// successfully created oidc client
// @ts-ignore private property
expect(await store.getOidcClient()).toBeTruthy();
});
it("should log and return when no clientId is found in storage", async () => {
localStorage.removeItem("mx_oidc_client_id");
const store = new OidcClientStore(mockClient);
// no oidc client
// @ts-ignore private property
expect(await store.getOidcClient()).toEqual(undefined);
expect(logger.error).toHaveBeenCalledWith(
"Failed to initialise OidcClientStore",
new Error("Oidc client id not found in storage"),
);
});
it("should log and return when discovery and validation fails", async () => {
mocked(discoverAndValidateOIDCIssuerWellKnown).mockRejectedValue(new Error(OidcError.OpSupport));
const store = new OidcClientStore(mockClient);
await store.readyPromise;
expect(logger.error).toHaveBeenCalledWith(
"Failed to initialise OidcClientStore",
new Error(OidcError.OpSupport),
);
// no oidc client
// @ts-ignore private property
expect(await store.getOidcClient()).toEqual(undefined);
});
it("should create oidc client correctly", async () => {
const store = new OidcClientStore(mockClient);
// @ts-ignore private property
const client = await store.getOidcClient();
expect(client?.settings.client_id).toEqual(clientId);
expect(client?.settings.authority).toEqual(authConfig.issuer);
});
it("should set account management endpoint when configured", async () => {
const store = new OidcClientStore(mockClient);
// @ts-ignore private property
await store.getOidcClient();
expect(store.accountManagementEndpoint).toEqual(account);
});
it("should set account management actions supported when configured", async () => {
const store = new OidcClientStore(mockClient);
// @ts-ignore private property
await store.getOidcClient();
expect(store.accountManagementActionsSupported).toEqual(accountManagementActionsSupported);
});
it("should set account management endpoint to issuer when not configured", async () => {
mocked(discoverAndValidateOIDCIssuerWellKnown)
.mockClear()
.mockResolvedValue({
...authConfig,
account_management_uri: undefined,
authorization_endpoint: "authorization-endpoint",
token_endpoint: "token-endpoint",
});
const store = new OidcClientStore(mockClient);
await store.readyPromise;
expect(store.accountManagementEndpoint).toEqual(authConfig.issuer);
});
it("should reuse initialised oidc client", async () => {
const store = new OidcClientStore(mockClient);
// @ts-ignore private property
store.getOidcClient();
// @ts-ignore private property
store.getOidcClient();
await flushPromises();
// finished initialising
// @ts-ignore private property
expect(await store.getOidcClient()).toBeTruthy();
// @ts-ignore private property
store.getOidcClient();
// only called once for multiple calls to getOidcClient
// before and after initialisation is complete
expect(discoverAndValidateOIDCIssuerWellKnown).toHaveBeenCalledTimes(1);
});
});
describe("revokeTokens()", () => {
const accessToken = "test-access-token";
const refreshToken = "test-refresh-token";
beforeEach(() => {
// spy and call through
jest.spyOn(OidcClient.prototype, "revokeToken").mockClear();
fetchMock.clearHistory();
fetchMock.removeRoute("revocation-endpoint");
fetchMock.post(
authConfig.revocation_endpoint,
{
status: 200,
},
{ name: "revocation-endpoint" },
);
});
it("should throw when oidcClient could not be initialised", async () => {
// make oidcClient initialisation fail
localStorage.removeItem("mx_oidc_token_issuer");
const store = new OidcClientStore(mockClient);
await expect(() => store.revokeTokens(accessToken, refreshToken)).rejects.toThrow("No OIDC client");
});
it("should revoke access and refresh tokens", async () => {
const store = new OidcClientStore(mockClient);
await store.revokeTokens(accessToken, refreshToken);
expect(fetchMock).toHaveFetchedTimes(2, authConfig.revocation_endpoint);
expect(OidcClient.prototype.revokeToken).toHaveBeenCalledWith(accessToken, "access_token");
expect(OidcClient.prototype.revokeToken).toHaveBeenCalledWith(refreshToken, "refresh_token");
});
it("should still attempt to revoke refresh token when access token revocation fails", async () => {
// fail once, then succeed
fetchMock.removeRoute("revocation-endpoint");
fetchMock
.postOnce(authConfig.revocation_endpoint, {
status: 404,
})
.post(authConfig.revocation_endpoint, {
status: 200,
});
const store = new OidcClientStore(mockClient);
await expect(() => store.revokeTokens(accessToken, refreshToken)).rejects.toThrow(
"Failed to revoke tokens",
);
expect(fetchMock).toHaveFetchedTimes(2, authConfig.revocation_endpoint);
expect(OidcClient.prototype.revokeToken).toHaveBeenCalledWith(accessToken, "access_token");
});
});
describe("OIDC Aware", () => {
beforeEach(() => {
localStorage.clear();
});
it("should resolve account management endpoint", async () => {
mockClient.getAuthMetadata.mockResolvedValue({
...authConfig,
account_management_uri: account,
account_management_actions_supported: accountManagementActionsSupported,
});
const store = new OidcClientStore(mockClient);
await store.readyPromise;
expect(store.accountManagementEndpoint).toBe(account);
expect(store.accountManagementActionsSupported).toEqual(accountManagementActionsSupported);
});
});
});
@@ -11,7 +11,7 @@ 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";
import { makeDelegatedAuthMetadata } from "../../test-utils/auth";
describe("AutoDiscoveryUtils", () => {
beforeEach(() => {
@@ -224,115 +224,15 @@ describe("AutoDiscoveryUtils", () => {
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": [
fetchMock.get(`${validHsConfig["m.homeserver"].base_url}/_matrix/client/versions`, { versions: ["v1.15"] });
fetchMock.get(`${validHsConfig["m.homeserver"].base_url}/_matrix/client/v1/auth_metadata`, {
...makeDelegatedAuthMetadata(issuer),
scopes_supported: ["email"],
response_modes_supported: ["query", "fragment"],
prompt_values_supported: ["none", "login", "create"],
device_authorization_endpoint: `${issuer}oauth2/device`,
account_management_uri: `${issuer}account/`,
account_management_actions_supported: [
"org.matrix.profile",
"org.matrix.sessions_list",
"org.matrix.session_view",
@@ -340,9 +240,6 @@ describe("AutoDiscoveryUtils", () => {
"org.matrix.cross_signing_reset",
],
});
fetchMock.get(`${issuer}jwks`, {
keys: [],
});
const discoveryResult = {
...validIsConfig,
@@ -366,7 +263,6 @@ describe("AutoDiscoveryUtils", () => {
account_management_uri: "https://auth.matrix.org/account/",
authorization_endpoint: "https://auth.matrix.org/auth",
registration_endpoint: "https://auth.matrix.org/registration",
signingKeys: [],
token_endpoint: "https://auth.matrix.org/token",
}),
warning: null,
@@ -6,36 +6,29 @@ 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.
*/
import fetchMock from "@fetch-mock/jest";
import { completeAuthorizationCodeGrant } from "matrix-js-sdk/src/oidc/authorize";
import { OAuth2, type BearerTokenResponse } from "matrix-js-sdk/src/matrix";
import * as randomStringUtils from "matrix-js-sdk/src/randomstring";
import { type BearerTokenResponse } from "matrix-js-sdk/src/oidc/validate";
import { mocked } from "jest-mock";
import { Crypto } from "@peculiar/webcrypto";
import { getRandomValues } from "node:crypto";
import { completeOidcLogin, startOidcLogin } from "../../../../src/utils/oidc/authorize";
import { makeDelegatedAuthConfig } from "../../../test-utils/oidc";
import { OidcClientError } from "../../../../src/utils/oidc/error";
import { completeOAuthLogin, startOAuthLogin } from "../../../../src/utils/oauth/authorize";
import { makeDelegatedAuthMetadata } from "../../../test-utils/auth";
import { OAuthClientError } from "../../../../src/utils/oauth/error";
import { mockPlatformPeg } from "../../../test-utils";
import { storeAuthContext } from "../../../../src/utils/oauth/persistOAuthSettings.ts";
jest.unmock("matrix-js-sdk/src/randomstring");
jest.mock("matrix-js-sdk/src/oidc/authorize", () => ({
...jest.requireActual("matrix-js-sdk/src/oidc/authorize"),
completeAuthorizationCodeGrant: jest.fn(),
}));
const webCrypto = new Crypto();
describe("OIDC authorization", () => {
describe("OAuth2 authorization", () => {
const issuer = "https://auth.com/";
const homeserverUrl = "https://matrix.org";
const identityServerUrl = "https://is.org";
const clientId = "xyz789";
const baseUrl = "https://test.com";
const delegatedAuthConfig = makeDelegatedAuthConfig(issuer);
const delegatedAuthConfig = makeDelegatedAuthMetadata(issuer);
// to restore later
const realWindowLocation = window.location;
@@ -58,8 +51,6 @@ describe("OIDC authorization", () => {
subtle: webCrypto.subtle,
},
});
fetchMock.get(`${delegatedAuthConfig.issuer}.well-known/openid-configuration`, delegatedAuthConfig);
});
afterAll(() => {
@@ -67,15 +58,15 @@ describe("OIDC authorization", () => {
window.location = realWindowLocation;
});
describe("startOidcLogin()", () => {
describe("startOAuthLogin()", () => {
it("navigates to authorization endpoint with correct parameters", async () => {
await startOidcLogin(delegatedAuthConfig, clientId, homeserverUrl);
await startOAuthLogin(delegatedAuthConfig, clientId, homeserverUrl);
const expectedScopeWithoutDeviceId = `openid urn:matrix:org.matrix.msc2967.client:api:* urn:matrix:org.matrix.msc2967.client:device:`;
const expectedScopeWithoutDeviceId = `urn:matrix:client:api:* urn:matrix:client:device:`;
const authUrl = new URL(window.location.href);
expect(authUrl.searchParams.get("response_mode")).toEqual("query");
expect(authUrl.searchParams.get("response_mode")).toEqual("fragment");
expect(authUrl.searchParams.get("response_type")).toEqual("code");
expect(authUrl.searchParams.get("client_id")).toEqual(clientId);
expect(authUrl.searchParams.get("code_challenge_method")).toEqual("S256");
@@ -87,12 +78,11 @@ describe("OIDC authorization", () => {
// random string, just check they are set
expect(authUrl.searchParams.has("state")).toBeTruthy();
expect(authUrl.searchParams.has("nonce")).toBeTruthy();
expect(authUrl.searchParams.has("code_challenge")).toBeTruthy();
});
it("should prefer response_mode fragment if supported", async () => {
await startOidcLogin(
await startOAuthLogin(
{ ...delegatedAuthConfig, response_modes_supported: ["query", "fragment"] },
clientId,
homeserverUrl,
@@ -104,68 +94,59 @@ describe("OIDC authorization", () => {
});
});
describe("completeOidcLogin()", () => {
describe("completeOAuth2Login()", () => {
const state = "test-state-444";
const code = "test-code-777";
const params = {
code,
state: state,
state,
};
const tokenResponse: BearerTokenResponse = {
access_token: "abc123",
refresh_token: "def456",
id_token: "ghi789",
scope: "test",
token_type: "Bearer",
expires_at: 12345,
expires_in: 12345,
};
beforeEach(() => {
mocked(completeAuthorizationCodeGrant)
.mockClear()
.mockResolvedValue({
oidcClientSettings: {
clientId,
issuer,
},
tokenResponse,
homeserverUrl,
identityServerUrl,
idTokenClaims: {
aud: "123",
iss: issuer,
sub: "123",
exp: 123,
iat: 456,
},
});
jest.spyOn(OAuth2.prototype, "completeAuthorizationCodeGrant").mockResolvedValue(tokenResponse);
storeAuthContext({
state,
homeserverUrl,
metadata: delegatedAuthConfig,
identityServerUrl,
authContext: {
codeVerifier: "123456",
clientId,
deviceId: "DEADB33F",
redirectUri: "https://test.com/callback",
},
});
});
it("should throw when query params do not include state and code", async () => {
await expect(async () => await completeOidcLogin({}, "query")).rejects.toThrow(
OidcClientError.InvalidQueryParameters,
it("should throw when fragment params do not include state and code", async () => {
await expect(async () => await completeOAuthLogin({})).rejects.toThrow(
OAuthClientError.InvalidFragmentParameters,
);
});
it("should make request complete authorization code grant", async () => {
await completeOidcLogin(params, "fragment");
await completeOAuthLogin(params);
expect(completeAuthorizationCodeGrant).toHaveBeenCalledWith(code, state, "fragment");
expect(OAuth2.prototype.completeAuthorizationCodeGrant).toHaveBeenCalledWith(code);
});
it("should return accessToken, configured homeserver and identityServer", async () => {
const result = await completeOidcLogin(params, "query");
const result = await completeOAuthLogin(params);
expect(result).toEqual({
accessToken: tokenResponse.access_token,
refreshToken: tokenResponse.refresh_token,
homeserverUrl,
identityServerUrl,
issuer,
clientId,
idToken: "ghi789",
idTokenClaims: result.idTokenClaims,
});
});
});
@@ -7,25 +7,24 @@ Please see LICENSE files in the repository root for full details.
*/
import fetchMock from "@fetch-mock/jest";
import { OidcError } from "matrix-js-sdk/src/oidc/error";
import { type OidcClientConfig } from "matrix-js-sdk/src/matrix";
import { OAuth2Error } from "matrix-js-sdk/src/matrix";
import { getOidcClientId } from "../../../../src/utils/oidc/registerClient";
import { getOAuthClientId } from "../../../../src/utils/oauth/registerClient";
import { mockPlatformPeg } from "../../../test-utils";
import PlatformPeg from "../../../../src/PlatformPeg";
import { makeDelegatedAuthConfig } from "../../../test-utils/oidc";
import { makeDelegatedAuthMetadata } from "../../../test-utils/auth";
describe("getOidcClientId()", () => {
describe("getOAuthClientId()", () => {
const issuer = "https://auth.com/";
const clientName = "Element";
const baseUrl = "https://just.testing";
const dynamicClientId = "xyz789";
const staticOidcClients = {
const staticOAuthClients = {
[issuer]: {
client_id: "abc123",
},
};
const delegatedAuthConfig = makeDelegatedAuthConfig(issuer);
const delegatedAuthConfig = makeDelegatedAuthMetadata(issuer);
beforeEach(() => {
fetchMock.removeRoutes();
@@ -35,12 +34,12 @@ describe("getOidcClientId()", () => {
return baseUrl;
},
});
Object.defineProperty(PlatformPeg.get(), "defaultOidcClientUri", {
Object.defineProperty(PlatformPeg.get(), "defaultOAuthClientUri", {
get(): string {
return baseUrl;
},
});
Object.defineProperty(PlatformPeg.get(), "getOidcCallbackUrl", {
Object.defineProperty(PlatformPeg.get(), "getOAuthCallbackUrl", {
value: () => ({
href: baseUrl,
}),
@@ -48,31 +47,7 @@ describe("getOidcClientId()", () => {
});
it("should return static clientId when configured", async () => {
expect(await getOidcClientId(delegatedAuthConfig, staticOidcClients)).toEqual("abc123");
// didn't try to register
expect(fetchMock).toHaveFetchedTimes(0);
});
it("should throw when no static clientId is configured and no registration endpoint", async () => {
const authConfigWithoutRegistration: OidcClientConfig = makeDelegatedAuthConfig(
"https://issuerWithoutStaticClientId.org/",
);
authConfigWithoutRegistration.registration_endpoint = undefined;
await expect(getOidcClientId(authConfigWithoutRegistration, staticOidcClients)).rejects.toThrow(
OidcError.DynamicRegistrationNotSupported,
);
// didn't try to register
expect(fetchMock).toHaveFetchedTimes(0);
});
it("should handle when staticOidcClients object is falsy", async () => {
const authConfigWithoutRegistration: OidcClientConfig = {
...delegatedAuthConfig,
registration_endpoint: undefined,
};
await expect(getOidcClientId(authConfigWithoutRegistration)).rejects.toThrow(
OidcError.DynamicRegistrationNotSupported,
);
expect(await getOAuthClientId(delegatedAuthConfig, staticOAuthClients)).toEqual("abc123");
// didn't try to register
expect(fetchMock).toHaveFetchedTimes(0);
});
@@ -82,7 +57,7 @@ describe("getOidcClientId()", () => {
status: 200,
body: JSON.stringify({ client_id: dynamicClientId }),
});
expect(await getOidcClientId(delegatedAuthConfig)).toEqual(dynamicClientId);
expect(await getOAuthClientId(delegatedAuthConfig)).toEqual(dynamicClientId);
// didn't try to register
expect(fetchMock).toHaveFetched(delegatedAuthConfig.registration_endpoint!, {
headers: {
@@ -96,7 +71,6 @@ describe("getOidcClientId()", () => {
response_types: ["code"],
grant_types: ["authorization_code", "refresh_token"],
redirect_uris: [baseUrl],
id_token_signed_response_alg: "RS256",
token_endpoint_auth_method: "none",
application_type: "web",
logo_uri: `${baseUrl}/vector-icons/1024.png`,
@@ -108,7 +82,7 @@ describe("getOidcClientId()", () => {
fetchMock.post(delegatedAuthConfig.registration_endpoint!, {
status: 500,
});
await expect(getOidcClientId(delegatedAuthConfig)).rejects.toThrow(OidcError.DynamicRegistrationFailed);
await expect(getOAuthClientId(delegatedAuthConfig)).rejects.toThrow(OAuth2Error.DynamicRegistrationFailed);
});
it("should throw when registration response is invalid", async () => {
@@ -117,6 +91,6 @@ describe("getOidcClientId()", () => {
// no clientId in response
body: "{}",
});
await expect(getOidcClientId(delegatedAuthConfig)).rejects.toThrow(OidcError.DynamicRegistrationInvalid);
await expect(getOAuthClientId(delegatedAuthConfig)).rejects.toThrow(OAuth2Error.DynamicRegistrationInvalid);
});
});
@@ -1,88 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import fetchMock from "@fetch-mock/jest";
import { mocked } from "jest-mock";
import { TokenRefresher } from "../../../../src/utils/oidc/TokenRefresher";
import { persistAccessTokenInStorage, persistRefreshTokenInStorage } from "../../../../src/utils/tokens/tokens";
import { mockPlatformPeg } from "../../../test-utils";
import { makeDelegatedAuthConfig } from "../../../test-utils/oidc";
jest.mock("../../../../src/utils/tokens/tokens", () => ({
persistAccessTokenInStorage: jest.fn(),
persistRefreshTokenInStorage: jest.fn(),
}));
describe("TokenRefresher", () => {
const clientId = "test-client-id";
const issuer = "https://auth.com/";
const redirectUri = "https://test.com";
const deviceId = "test-device-id";
const userId = "@alice:server.org";
const accessToken = "test-access-token";
const refreshToken = "test-refresh-token";
const authConfig = makeDelegatedAuthConfig(issuer);
const idTokenClaims = {
aud: "123",
iss: issuer,
sub: "123",
exp: 123,
iat: 456,
};
beforeEach(() => {
fetchMock.get(`${issuer}.well-known/openid-configuration`, authConfig);
fetchMock.get(`${issuer}jwks`, {
status: 200,
headers: {
"Content-Type": "application/json",
},
keys: [],
});
mocked(persistAccessTokenInStorage).mockResolvedValue(undefined);
mocked(persistRefreshTokenInStorage).mockResolvedValue(undefined);
});
afterEach(() => {
jest.restoreAllMocks();
});
it("should persist tokens with a pickle key", async () => {
const pickleKey = "test-pickle-key";
const getPickleKey = jest.fn().mockResolvedValue(pickleKey);
mockPlatformPeg({ getPickleKey });
const refresher = new TokenRefresher(issuer, clientId, redirectUri, deviceId, idTokenClaims, userId);
await refresher.oidcClientReady;
await refresher.persistTokens({ accessToken, refreshToken });
expect(getPickleKey).toHaveBeenCalledWith(userId, deviceId);
expect(persistAccessTokenInStorage).toHaveBeenCalledWith(accessToken, pickleKey);
expect(persistRefreshTokenInStorage).toHaveBeenCalledWith(refreshToken, pickleKey);
});
it("should persist tokens without a pickle key", async () => {
const getPickleKey = jest.fn().mockResolvedValue(null);
mockPlatformPeg({ getPickleKey });
const refresher = new TokenRefresher(issuer, clientId, redirectUri, deviceId, idTokenClaims, userId);
await refresher.oidcClientReady;
await refresher.persistTokens({ accessToken, refreshToken });
expect(getPickleKey).toHaveBeenCalledWith(userId, deviceId);
expect(persistAccessTokenInStorage).toHaveBeenCalledWith(accessToken, undefined);
expect(persistRefreshTokenInStorage).toHaveBeenCalledWith(refreshToken, undefined);
});
});
@@ -63,15 +63,12 @@ describe("UserMenuViewModel", () => {
});
it("should show a link for account management", async () => {
const vm = new UserMenuViewModel(
{ ownProfileStore: mockOwnProfileStore },
dispatcher,
client,
true,
"https://example.org/",
);
client.getAuthMetadata.mockResolvedValue({ account_management_uri: "https://example.org/" } as any);
const vm = new UserMenuViewModel({ ownProfileStore: mockOwnProfileStore }, dispatcher, client, true);
vm.setOpen(true);
expect(vm.getSnapshot().manageAccountHref).toEqual("https://example.org/");
await waitFor(() => {
expect(vm.getSnapshot().manageAccountHref).toEqual("https://example.org/");
});
});
it("should generate a menu options for a guest", () => {