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:
co-authored by
Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
parent
38e29c51c4
commit
2bc9656957
@@ -119,8 +119,15 @@ jobs:
|
||||
node-version: "lts/*"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install Deps
|
||||
- name: Install Deps (layered)
|
||||
run: "./scripts/layered.sh"
|
||||
if: matrix.path != 'packages/shared-components'
|
||||
env:
|
||||
JS_SDK_GITHUB_BASE_REF: ${{ inputs.matrix-js-sdk-sha }}
|
||||
|
||||
- name: Install Deps (normal)
|
||||
run: "pnpm install"
|
||||
if: matrix.path == 'packages/shared-components'
|
||||
|
||||
- name: Cache storybook & vitest
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
|
||||
|
||||
@@ -9,7 +9,7 @@ import { test, expect } from "../../element-desktop-test.js";
|
||||
|
||||
declare global {
|
||||
interface ElectronPlatform {
|
||||
getOidcCallbackUrl(): URL;
|
||||
getOAuthCallbackUrl(): URL;
|
||||
}
|
||||
|
||||
interface Window {
|
||||
@@ -29,7 +29,7 @@ test.describe("OIDC Native", () => {
|
||||
test("should use OIDC callback URL without authority component", async ({ page }) => {
|
||||
await expect(
|
||||
page.evaluate<string>(() => {
|
||||
return window.mxPlatformPeg.get().getOidcCallbackUrl().toString();
|
||||
return window.mxPlatformPeg.get().getOAuthCallbackUrl().toString();
|
||||
}),
|
||||
).resolves.toMatch(/io\.element\.(desktop|nightly):\/vector\/webapp\//);
|
||||
});
|
||||
|
||||
@@ -138,12 +138,6 @@ module.exports = {
|
||||
"!matrix-js-sdk/src/extensible_events_v1/PollResponseEvent",
|
||||
"!matrix-js-sdk/src/extensible_events_v1/PollEndEvent",
|
||||
"!matrix-js-sdk/src/extensible_events_v1/InvalidEventError",
|
||||
"!matrix-js-sdk/src/oidc",
|
||||
"!matrix-js-sdk/src/oidc/discovery",
|
||||
"!matrix-js-sdk/src/oidc/authorize",
|
||||
"!matrix-js-sdk/src/oidc/validate",
|
||||
"!matrix-js-sdk/src/oidc/error",
|
||||
"!matrix-js-sdk/src/oidc/register",
|
||||
"!matrix-js-sdk/src/webrtc",
|
||||
"!matrix-js-sdk/src/webrtc/call",
|
||||
"!matrix-js-sdk/src/webrtc/callFeed",
|
||||
|
||||
@@ -87,7 +87,6 @@
|
||||
"matrix-widget-api": "^1.16.1",
|
||||
"memoize-one": "^6.0.0",
|
||||
"mime": "^4.0.4",
|
||||
"oidc-client-ts": "^3.0.1",
|
||||
"opus-recorder": "^8.0.3",
|
||||
"pako": "^3.0.0",
|
||||
"png-chunks-extract": "^1.0.0",
|
||||
|
||||
@@ -92,8 +92,10 @@ test.describe("Account user settings tab", () => {
|
||||
authorization_endpoint: `${EXTERNAL_ACCOUNT_MANAGEMENT_URL}authorize`,
|
||||
token_endpoint: `${EXTERNAL_ACCOUNT_MANAGEMENT_URL}token`,
|
||||
revocation_endpoint: `${EXTERNAL_ACCOUNT_MANAGEMENT_URL}revoke`,
|
||||
registration_endpoint: `${EXTERNAL_ACCOUNT_MANAGEMENT_URL}register`,
|
||||
response_types_supported: ["code"],
|
||||
grant_types_supported: ["authorization_code"],
|
||||
grant_types_supported: ["authorization_code", "refresh_token"],
|
||||
response_modes_supported: ["query", "fragment"],
|
||||
code_challenge_methods_supported: ["S256"],
|
||||
account_management_uri: EXTERNAL_ACCOUNT_MANAGEMENT_URL,
|
||||
},
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
type Room,
|
||||
type SSOAction,
|
||||
encodeUnpaddedBase64,
|
||||
type OidcRegistrationClientMetadata,
|
||||
type OAuthRegistrationRequest,
|
||||
MatrixEventEvent,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
@@ -431,42 +431,41 @@ export default abstract class BasePlatform {
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback Client URI to use for OIDC client registration for if one is not specified in config.json
|
||||
* Fallback Client URI to use for OAuth2 client registration for if one is not specified in config.json
|
||||
*/
|
||||
public get defaultOidcClientUri(): string {
|
||||
public get defaultOAuthClientUri(): string {
|
||||
return window.location.origin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Metadata to use for dynamic OIDC client registrations
|
||||
* Metadata to use for dynamic OAuth2 client registrations
|
||||
*/
|
||||
public async getOidcClientMetadata(): Promise<OidcRegistrationClientMetadata> {
|
||||
public async getOAuthClientMetadata(): Promise<OAuthRegistrationRequest> {
|
||||
const config = SdkConfig.get();
|
||||
return {
|
||||
clientName: config.brand,
|
||||
clientUri: config.oidc_metadata?.client_uri ?? this.defaultOidcClientUri,
|
||||
redirectUris: [this.getOidcCallbackUrl().href],
|
||||
logoUri: config.oidc_metadata?.logo_uri ?? new URL("vector-icons/1024.png", this.baseUrl).href,
|
||||
applicationType: "web",
|
||||
contacts: config.oidc_metadata?.contacts,
|
||||
tosUri: config.oidc_metadata?.tos_uri ?? config.terms_and_conditions_links?.[0]?.url,
|
||||
policyUri: config.oidc_metadata?.policy_uri ?? config.privacy_policy_url,
|
||||
client_name: config.brand,
|
||||
client_uri: config.oidc_metadata?.client_uri ?? this.defaultOAuthClientUri,
|
||||
redirect_uris: [this.getOAuthCallbackUrl().href],
|
||||
logo_uri: config.oidc_metadata?.logo_uri ?? new URL("vector-icons/1024.png", this.baseUrl).href,
|
||||
application_type: "web",
|
||||
tos_uri: config.oidc_metadata?.tos_uri ?? config.terms_and_conditions_links?.[0]?.url,
|
||||
policy_uri: config.oidc_metadata?.policy_uri ?? config.privacy_policy_url,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Suffix to append to the `state` parameter of OIDC /auth calls. Will be round-tripped to the callback URI.
|
||||
* Suffix to append to the `state` parameter of OAuth2 /auth calls. Will be round-tripped to the callback URI.
|
||||
* Currently only required for ElectronPlatform for passing element-desktop-ssoid.
|
||||
*/
|
||||
public getOidcClientState(): string {
|
||||
public getOAuthClientState(): string {
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* The URL to return to after a successful OIDC authentication
|
||||
* The URL to return to after a successful OAuth2 authentication
|
||||
*/
|
||||
public getOidcCallbackUrl(): URL {
|
||||
// The redirect URL has to exactly match that registered at the OIDC server, so
|
||||
public getOAuthCallbackUrl(): URL {
|
||||
// The redirect URL has to exactly match that registered at the OAuth2 server, so
|
||||
// build it from scratch to avoid leaking ephemeral query params (e.g. `updated`).
|
||||
const url = new URL(window.location.origin + window.location.pathname);
|
||||
// Set no_universal_links=true to prevent the callback being handled by Element X installed on macOS Apple Silicon
|
||||
|
||||
@@ -11,11 +11,17 @@ Please see LICENSE files in the repository root for full details.
|
||||
import { vi, describe, it, expect, beforeEach, afterEach, type MockedObject } from "vitest";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import * as MatrixJs from "matrix-js-sdk/src/matrix";
|
||||
import { decodeBase64, encodeUnpaddedBase64 } from "matrix-js-sdk/src/matrix";
|
||||
import { decodeBase64, encodeUnpaddedBase64, MatrixClient, OAuth2 } from "matrix-js-sdk/src/matrix";
|
||||
import * as encryptAESSecretStorageItemModule from "matrix-js-sdk/src/utils/encryptAESSecretStorageItem";
|
||||
import fetchMock from "@fetch-mock/vitest";
|
||||
import { flushPromises, getMockClientWithEventEmitter, mockClientMethodsUser, mockPlatformPeg } from "test-utils";
|
||||
import { makeDelegatedAuthConfig } from "test-utils/oidc";
|
||||
import {
|
||||
flushPromises,
|
||||
getMockClientWithEventEmitter,
|
||||
mockClientMethodsUser,
|
||||
mockClientMethodsServer,
|
||||
mockPlatformPeg,
|
||||
} from "test-utils";
|
||||
import { makeDelegatedAuthMetadata } from "test-utils/auth";
|
||||
|
||||
import StorageEvictedDialog from "./components/views/dialogs/StorageEvictedDialog";
|
||||
import * as Lifecycle from "./Lifecycle";
|
||||
@@ -23,10 +29,9 @@ import { MatrixClientPeg } from "./MatrixClientPeg";
|
||||
import Modal from "./Modal";
|
||||
import * as StorageAccess from "./utils/StorageAccess";
|
||||
import { idbSave } from "./utils/StorageAccess";
|
||||
import { OidcClientStore } from "./stores/oidc/OidcClientStore";
|
||||
import { Action } from "./dispatcher/actions";
|
||||
import PlatformPeg from "./PlatformPeg";
|
||||
import { persistAccessTokenInStorage, persistRefreshTokenInStorage } from "./utils/tokens/tokens";
|
||||
import { persistTokens } from "./utils/tokens/tokens";
|
||||
import { encryptPickleKey } from "./utils/tokens/pickling";
|
||||
import * as StorageManager from "./utils/StorageManager.ts";
|
||||
import type BasePlatform from "./BasePlatform.ts";
|
||||
@@ -49,22 +54,19 @@ describe("Lifecycle", () => {
|
||||
mockPlatform = mockPlatformPeg();
|
||||
mockClient = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(),
|
||||
...mockClientMethodsServer(),
|
||||
stopClient: vi.fn(),
|
||||
removeAllListeners: vi.fn(),
|
||||
clearStores: vi.fn(),
|
||||
getAccountData: vi.fn(),
|
||||
getDeviceId: vi.fn().mockReturnValue(deviceId),
|
||||
isVersionSupported: vi.fn().mockResolvedValue(true),
|
||||
getCrypto: vi.fn(),
|
||||
getClientWellKnown: vi.fn(),
|
||||
waitForClientWellKnown: vi.fn(),
|
||||
getThirdpartyProtocols: vi.fn(),
|
||||
store: {
|
||||
destroy: vi.fn(),
|
||||
},
|
||||
getVersions: vi.fn().mockResolvedValue({ versions: ["v1.1"] }),
|
||||
logout: vi.fn().mockResolvedValue(undefined),
|
||||
getAccessToken: vi.fn(),
|
||||
getRefreshToken: vi.fn(),
|
||||
isInitialSyncComplete: vi.fn(),
|
||||
setGuest: vi.fn(),
|
||||
@@ -78,6 +80,7 @@ describe("Lifecycle", () => {
|
||||
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
vi.spyOn(MatrixClient.prototype, "getAuthMetadata").mockResolvedValue(makeDelegatedAuthMetadata());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -119,8 +122,6 @@ describe("Lifecycle", () => {
|
||||
mx_is_url: identityServerUrl,
|
||||
mx_user_id: userId,
|
||||
mx_device_id: deviceId,
|
||||
mx_oidc_token_issuer: "test-issuer.dummy",
|
||||
mx_oidc_client_id: "test-client-id",
|
||||
};
|
||||
const idbStorageSession = {
|
||||
account: {
|
||||
@@ -304,6 +305,7 @@ describe("Lifecycle", () => {
|
||||
describe("with a refresh token", () => {
|
||||
beforeEach(() => {
|
||||
localStorage.setItem("mx_refresh_token", refreshToken);
|
||||
localStorage.setItem("mx_oidc_client_id", "test-client-id");
|
||||
for (const key in localStorageSession) {
|
||||
localStorage.setItem(key, localStorageSession[key]);
|
||||
}
|
||||
@@ -334,7 +336,7 @@ describe("Lifecycle", () => {
|
||||
guest: false,
|
||||
pickleKey: undefined,
|
||||
},
|
||||
expect.any(Function),
|
||||
expect.any(OAuth2),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -344,6 +346,7 @@ describe("Lifecycle", () => {
|
||||
let pickleKey: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
localStorage.setItem("mx_oidc_client_id", "test-client-id");
|
||||
for (const key in localStorageSession) {
|
||||
localStorage.setItem(key, localStorageSession[key]);
|
||||
}
|
||||
@@ -355,7 +358,7 @@ describe("Lifecycle", () => {
|
||||
// Indicate that we should have a pickle key
|
||||
localStorage.setItem("mx_has_pickle_key", "true");
|
||||
|
||||
await persistAccessTokenInStorage(credentials.accessToken, pickleKey);
|
||||
await persistTokens(pickleKey, credentials);
|
||||
});
|
||||
|
||||
it("should persist credentials", async () => {
|
||||
@@ -415,7 +418,7 @@ describe("Lifecycle", () => {
|
||||
guest: false,
|
||||
pickleKey,
|
||||
},
|
||||
undefined,
|
||||
expect.any(OAuth2),
|
||||
);
|
||||
|
||||
expect(MatrixClientPeg.start).toHaveBeenCalledWith({ rustCryptoStoreKey: expect.any(Uint8Array) });
|
||||
@@ -423,7 +426,7 @@ describe("Lifecycle", () => {
|
||||
|
||||
describe("with a refresh token", () => {
|
||||
beforeEach(async () => {
|
||||
await persistRefreshTokenInStorage(refreshToken, pickleKey);
|
||||
await persistTokens(pickleKey, { ...credentials, refreshToken });
|
||||
});
|
||||
|
||||
it("should persist credentials", async () => {
|
||||
@@ -454,7 +457,7 @@ describe("Lifecycle", () => {
|
||||
guest: false,
|
||||
pickleKey: pickleKey,
|
||||
},
|
||||
expect.any(Function),
|
||||
expect.any(OAuth2),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -486,7 +489,7 @@ describe("Lifecycle", () => {
|
||||
// Indicate that we should have a pickle key
|
||||
localStorage.setItem("mx_has_pickle_key", "true");
|
||||
|
||||
await persistAccessTokenInStorage(credentials.accessToken, pickleKey);
|
||||
await persistTokens(pickleKey, credentials);
|
||||
});
|
||||
|
||||
it("should create and start new matrix client with credentials", async () => {
|
||||
@@ -532,7 +535,7 @@ describe("Lifecycle", () => {
|
||||
// Create a pickle key, and store it, encrypted, in IDB.
|
||||
const pickleKey = (await PlatformPeg.get()!.createPickleKey(credentials.userId, credentials.deviceId))!;
|
||||
localStorage.setItem("mx_has_pickle_key", "true");
|
||||
await persistAccessTokenInStorage(credentials.accessToken, pickleKey);
|
||||
await persistTokens(pickleKey, credentials);
|
||||
|
||||
// Now destroy the pickle key
|
||||
await PlatformPeg.get()!.destroyPickleKey(credentials.userId, credentials.deviceId);
|
||||
@@ -624,7 +627,6 @@ describe("Lifecycle", () => {
|
||||
});
|
||||
|
||||
it("should persist a refreshToken when present", async () => {
|
||||
localStorage.setItem("mx_oidc_token_issuer", "test-issuer.dummy");
|
||||
localStorage.setItem("mx_oidc_client_id", "test-client-id");
|
||||
|
||||
await setLoggedIn({
|
||||
@@ -777,34 +779,18 @@ describe("Lifecycle", () => {
|
||||
const clientId = "test-client-id";
|
||||
const issuer = "https://auth.com/";
|
||||
|
||||
const delegatedAuthConfig = makeDelegatedAuthConfig(issuer);
|
||||
const idToken =
|
||||
"eyJhbGciOiJSUzI1NiIsImtpZCI6Imh4ZEhXb0Y5bW4ifQ.eyJzdWIiOiIwMUhQUDJGU0JZREU5UDlFTU04REQ3V1pIUiIsImlzcyI6Imh0dHBzOi8vYXV0aC1vaWRjLmxhYi5lbGVtZW50LmRldi8iLCJpYXQiOjE3MTUwNzE5ODUsImF1dGhfdGltZSI6MTcwNzk5MDMxMiwiY19oYXNoIjoidGt5R1RhUjU5aTk3YXoyTU4yMGdidyIsImV4cCI6MTcxNTA3NTU4NSwibm9uY2UiOiJxaXhwM0hFMmVaIiwiYXVkIjoiMDFIWDk0Mlg3QTg3REgxRUs2UDRaNjI4WEciLCJhdF9oYXNoIjoiNFlFUjdPRlVKTmRTeEVHV2hJUDlnZyJ9.HxODneXvSTfWB5Vc4cf7b8GiN2gdwUuTiyVqZuupWske2HkZiJZUt5Lsxg9BW3gz28POkE0Ln17snlkmy02B_AD3DQxKOOxQCzIIARHdfFvZxgGWsMdFcVQZDW7rtXcqgj-SpVaUQ_8acsgxSrz_DF2o0O4tto0PT6wVUiw8KlBmgWTscWPeAWe-39T-8EiQ8Wi16h6oSPcz2NzOQ7eOM_S9fDkOorgcBkRGLl1nrahrPSdWJSGAeruk5mX4YxN714YThFDyEA2t9YmKpjaiSQ2tT-Xkd7tgsZqeirNs2ni9mIiFX3bRX6t2AhUNzA7MaX9ZyizKGa6go3BESO_oDg";
|
||||
const delegatedAuthConfig = makeDelegatedAuthMetadata(issuer);
|
||||
|
||||
beforeEach(() => {
|
||||
fetchMock.get(`${delegatedAuthConfig.issuer}.well-known/openid-configuration`, delegatedAuthConfig);
|
||||
fetchMock.get(`${delegatedAuthConfig.issuer}jwks`, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
keys: [],
|
||||
});
|
||||
|
||||
// set values in local storage as they would be after a successful oidc authentication
|
||||
localStorage.setItem("mx_oidc_client_id", clientId);
|
||||
localStorage.setItem("mx_oidc_token_issuer", issuer);
|
||||
localStorage.setItem("mx_oidc_id_token", idToken);
|
||||
});
|
||||
|
||||
it("should not try to create a token refresher without a refresh token", async () => {
|
||||
await setLoggedIn(credentials);
|
||||
const cli = await setLoggedIn(credentials);
|
||||
|
||||
// didn't try to initialise token refresher
|
||||
expect(fetchMock).toHaveFetchedTimes(
|
||||
0,
|
||||
`${delegatedAuthConfig.issuer}.well-known/openid-configuration`,
|
||||
);
|
||||
expect(cli.http.opts.tokenRefreshFunction).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should not try to create a token refresher without a deviceId", async () => {
|
||||
@@ -824,7 +810,6 @@ describe("Lifecycle", () => {
|
||||
});
|
||||
|
||||
it("should not try to create a token refresher without an issuer in session storage", async () => {
|
||||
localStorage.removeItem("mx_oidc_token_issuer");
|
||||
await expect(
|
||||
setLoggedIn({
|
||||
...credentials,
|
||||
@@ -881,20 +866,16 @@ describe("Lifecycle", () => {
|
||||
});
|
||||
|
||||
describe("logout()", () => {
|
||||
let oidcClientStore!: OidcClientStore;
|
||||
const accessToken = "test-access-token";
|
||||
const refreshToken = "test-refresh-token";
|
||||
|
||||
beforeEach(() => {
|
||||
oidcClientStore = new OidcClientStore(mockClient);
|
||||
// stub
|
||||
vi.spyOn(oidcClientStore, "revokeTokens").mockResolvedValue(undefined);
|
||||
|
||||
mockClient.getAccessToken.mockReturnValue(accessToken);
|
||||
mockClient.getRefreshToken.mockReturnValue(refreshToken);
|
||||
vi.spyOn(OAuth2.prototype, "revokeToken").mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it("should call logout on the client when oidcClientStore is falsy", async () => {
|
||||
it("should call logout on the client when oauth is not used", async () => {
|
||||
logout();
|
||||
|
||||
await flushPromises();
|
||||
@@ -902,24 +883,15 @@ describe("Lifecycle", () => {
|
||||
expect(mockClient.logout).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it("should call logout on the client when oidcClientStore.isUserAuthenticatedWithOidc is falsy", async () => {
|
||||
vi.spyOn(oidcClientStore, "isUserAuthenticatedWithOidc", "get").mockReturnValue(false);
|
||||
logout(oidcClientStore);
|
||||
|
||||
await flushPromises();
|
||||
|
||||
expect(mockClient.logout).toHaveBeenCalledWith(true);
|
||||
expect(oidcClientStore.revokeTokens).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should revoke tokens when user is authenticated with oidc", async () => {
|
||||
vi.spyOn(oidcClientStore, "isUserAuthenticatedWithOidc", "get").mockReturnValue(true);
|
||||
logout(oidcClientStore);
|
||||
it("should revoke tokens when user is authenticated with oauth2", async () => {
|
||||
localStorage.setItem("mx_oidc_client_id", "test-client-id");
|
||||
logout();
|
||||
|
||||
await flushPromises();
|
||||
|
||||
expect(mockClient.logout).not.toHaveBeenCalled();
|
||||
expect(oidcClientStore.revokeTokens).toHaveBeenCalledWith(accessToken, refreshToken);
|
||||
expect(OAuth2.prototype.revokeToken).toHaveBeenCalledWith(accessToken, "access_token");
|
||||
expect(OAuth2.prototype.revokeToken).toHaveBeenCalledWith(refreshToken, "refresh_token");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+72
-109
@@ -10,13 +10,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type ReactNode } from "react";
|
||||
import {
|
||||
createClient,
|
||||
type MatrixClient,
|
||||
SSOAction,
|
||||
type OidcTokenRefresher,
|
||||
decodeBase64,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { MatrixClient, OAuth2, createClient, SSOAction, decodeBase64 } from "matrix-js-sdk/src/matrix";
|
||||
import { type AESEncryptedSecretStoragePayload } from "matrix-js-sdk/src/types";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
@@ -56,30 +50,23 @@ import { Action } from "./dispatcher/actions";
|
||||
import { type OverwriteLoginPayload } from "./dispatcher/payloads/OverwriteLoginPayload";
|
||||
import { SDKContextClass } from "./contexts/SDKContextClass";
|
||||
import { messageForLoginError } from "./utils/ErrorUtils";
|
||||
import { completeOidcLogin, type CompleteOidcLoginResponse } from "./utils/oidc/authorize";
|
||||
import { getOidcErrorMessage } from "./utils/oidc/error";
|
||||
import { type OidcClientStore } from "./stores/oidc/OidcClientStore";
|
||||
import {
|
||||
getStoredOidcClientId,
|
||||
getStoredOidcIdTokenClaims,
|
||||
getStoredOidcTokenIssuer,
|
||||
persistOidcAuthenticatedSettings,
|
||||
} from "./utils/oidc/persistOidcSettings";
|
||||
import { completeOAuthLogin, type CompleteOAuthLoginResponse } from "./utils/oauth/authorize";
|
||||
import { getOAuthErrorMessage } from "./utils/oauth/error";
|
||||
import { getOAuthParams, getStoredOAuthClientId, persistOAuthClientId } from "./utils/oauth/persistOAuthSettings";
|
||||
import {
|
||||
ACCESS_TOKEN_IV,
|
||||
ACCESS_TOKEN_STORAGE_KEY,
|
||||
HAS_ACCESS_TOKEN_STORAGE_KEY,
|
||||
HAS_REFRESH_TOKEN_STORAGE_KEY,
|
||||
persistAccessTokenInStorage,
|
||||
persistRefreshTokenInStorage,
|
||||
persistTokens,
|
||||
REFRESH_TOKEN_IV,
|
||||
REFRESH_TOKEN_STORAGE_KEY,
|
||||
tryDecryptToken,
|
||||
} from "./utils/tokens/tokens";
|
||||
import { TokenRefresher } from "./utils/oidc/TokenRefresher";
|
||||
import { checkBrowserSupport } from "./SupportedBrowser";
|
||||
import { type URLParams } from "./vector/url_utils.ts";
|
||||
import { type OnLoggedInPayload } from "./dispatcher/payloads/OnLoggedInPayload.ts";
|
||||
import { filterBoolean } from "./utils/arrays.ts";
|
||||
|
||||
const HOMESERVER_URL_KEY = "mx_hs_url";
|
||||
const ID_SERVER_URL_KEY = "mx_is_url";
|
||||
@@ -263,7 +250,7 @@ export async function getStoredSessionOwner(): Promise<[string, boolean] | [null
|
||||
}
|
||||
|
||||
/**
|
||||
* If query string includes OIDC authorization code flow parameters attempt to login using oidc flow
|
||||
* If query string includes OAuth2 authorization code flow parameters attempt to login using oauth flow
|
||||
* Else, we may be returning from SSO - attempt token login
|
||||
*
|
||||
* @param urlParams the parameters read in at app load time from the url
|
||||
@@ -279,30 +266,24 @@ export async function attemptDelegatedAuthLogin(
|
||||
defaultDeviceDisplayName?: string,
|
||||
fragmentAfterLogin?: string,
|
||||
): Promise<boolean> {
|
||||
if (urlParams.oidc_fragment) {
|
||||
return attemptOidcNativeLogin(urlParams.oidc_fragment, "fragment");
|
||||
} else if (urlParams.oidc_query) {
|
||||
return attemptOidcNativeLogin(urlParams.oidc_query, "query");
|
||||
if (urlParams.oauth2) {
|
||||
return attemptOAuthLogin(urlParams.oauth2);
|
||||
}
|
||||
|
||||
return attemptTokenLogin(urlParams["legacy_sso"], defaultDeviceDisplayName, fragmentAfterLogin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to login by completing OIDC authorization code flow
|
||||
* @param urlParams subset of app-load url parameters relating to oidc auth
|
||||
* @param responseMode - the response_mode used in the auth request
|
||||
* Attempt to login by completing OAuth2 authorization code flow
|
||||
* @param urlParams subset of app-load url parameters relating to oauth auth
|
||||
* @returns Promise that resolves to true when login succeeded, else false
|
||||
*/
|
||||
async function attemptOidcNativeLogin(
|
||||
urlParams: NonNullable<URLParams["oidc_fragment"]>,
|
||||
responseMode: "fragment" | "query",
|
||||
): Promise<boolean> {
|
||||
console.log("We have OIDC params - attempting OIDC login");
|
||||
async function attemptOAuthLogin(urlParams: NonNullable<URLParams["oauth2"]>): Promise<boolean> {
|
||||
console.log("We have OAuth2 params - attempting login");
|
||||
|
||||
try {
|
||||
const { accessToken, refreshToken, homeserverUrl, identityServerUrl, idToken, clientId, issuer } =
|
||||
await completeOidcLogin(urlParams, responseMode);
|
||||
const { accessToken, refreshToken, homeserverUrl, identityServerUrl, clientId } =
|
||||
await completeOAuthLogin(urlParams);
|
||||
|
||||
await configureFromCompletedOAuthLogin({
|
||||
accessToken,
|
||||
@@ -310,22 +291,20 @@ async function attemptOidcNativeLogin(
|
||||
homeserverUrl,
|
||||
identityServerUrl,
|
||||
clientId,
|
||||
issuer,
|
||||
idToken,
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error("Failed to login via OIDC", error);
|
||||
logger.error("Failed to login via OAuth", error);
|
||||
|
||||
onFailedDelegatedAuthLogin(getOidcErrorMessage(error as Error));
|
||||
onFailedDelegatedAuthLogin(getOAuthErrorMessage(error as Error));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exchange the given OIDC credentials for {@link IMatrixClientCreds}, additionally persisting them to storage.
|
||||
* @param creds the credentials from the OIDC flow
|
||||
* Exchange the given OAuth2 credentials for {@link IMatrixClientCreds}, additionally persisting them to storage.
|
||||
* @param creds the credentials from the OAuth2 flow
|
||||
*/
|
||||
export async function configureFromCompletedOAuthLogin({
|
||||
accessToken,
|
||||
@@ -333,9 +312,7 @@ export async function configureFromCompletedOAuthLogin({
|
||||
homeserverUrl,
|
||||
identityServerUrl,
|
||||
clientId,
|
||||
issuer,
|
||||
idToken,
|
||||
}: Omit<CompleteOidcLoginResponse, "idTokenClaims">): Promise<IMatrixClientCreds> {
|
||||
}: CompleteOAuthLoginResponse): Promise<IMatrixClientCreds> {
|
||||
const {
|
||||
user_id: userId,
|
||||
device_id: deviceId,
|
||||
@@ -352,10 +329,10 @@ export async function configureFromCompletedOAuthLogin({
|
||||
isGuest,
|
||||
};
|
||||
|
||||
logger.debug("Logged in via OIDC native flow");
|
||||
logger.debug("Logged in via OAuth2 native flow");
|
||||
await onSuccessfulDelegatedAuthLogin(credentials);
|
||||
// this needs to happen after success handler which clears storages
|
||||
persistOidcAuthenticatedSettings(clientId, issuer, idToken);
|
||||
persistOAuthClientId(clientId);
|
||||
return credentials;
|
||||
}
|
||||
|
||||
@@ -478,7 +455,7 @@ async function loadOrCreatePickleKey(credentials: IMatrixClientCreds): Promise<s
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after a successful token login or OIDC authorization.
|
||||
* Called after a successful token login or OAuth2 authorization.
|
||||
* Clear storage then save new credentials in storage
|
||||
* @param credentials as returned from login
|
||||
*/
|
||||
@@ -495,7 +472,7 @@ async function onSuccessfulDelegatedAuthLogin(credentials: IMatrixClientCreds):
|
||||
type TryAgainFunction = () => void;
|
||||
|
||||
/**
|
||||
* Display a friendly error to the user when token login or OIDC authorization fails
|
||||
* Display a friendly error to the user when token login or OAuth2 authorization fails
|
||||
* @param description error description
|
||||
* @param tryAgain OPTIONAL function to call on try again button from error dialog
|
||||
*/
|
||||
@@ -730,9 +707,9 @@ async function handleLoadSessionFailure(e: unknown, loadSessionOpts?: ILoadSessi
|
||||
* Also stops the old MatrixClient and clears old credentials/etc out of
|
||||
* storage before starting the new client.
|
||||
*
|
||||
* This function does not work for OIDC login.
|
||||
* This function does not work for OAuth2 login.
|
||||
* Storage is cleared early in the process so the required data is lost.
|
||||
* You must use {@link attemptDelegatedAuthLogin} followed by {@link restoreSessionFromStorage} for OIDC login.
|
||||
* You must use {@link attemptDelegatedAuthLogin} followed by {@link restoreSessionFromStorage} for OAuth2 login.
|
||||
*
|
||||
* @param {IMatrixClientCreds} credentials The credentials to use
|
||||
*
|
||||
@@ -782,44 +759,6 @@ export async function hydrateSession(credentials: IMatrixClientCreds): Promise<M
|
||||
return doSetLoggedIn(credentials, overwrite, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* When we have a authenticated via OIDC-native flow and have a refresh token
|
||||
* try to create a token refresher.
|
||||
* @param credentials from current session
|
||||
* @param clientId OIDC client ID
|
||||
* @throws If credentials.refreshToken or credentials.deviceId is falsy, or if no token issuer is stored
|
||||
* @returns Promise that resolves to a TokenRefresher
|
||||
*/
|
||||
async function createOidcTokenRefresher(
|
||||
credentials: IMatrixClientCreds,
|
||||
clientId: string,
|
||||
): Promise<OidcTokenRefresher> {
|
||||
if (!credentials.refreshToken) {
|
||||
throw new Error("A refresh token must be supplied in order to create an OIDC token refresher.");
|
||||
}
|
||||
// stored token issuer indicates we authenticated via OIDC-native flow
|
||||
const tokenIssuer = getStoredOidcTokenIssuer();
|
||||
if (!tokenIssuer) {
|
||||
throw new Error("Cannot create an OIDC token refresher as no stored OIDC token issuer was found.");
|
||||
}
|
||||
|
||||
const idTokenClaims = getStoredOidcIdTokenClaims();
|
||||
const redirectUri = PlatformPeg.get()!.getOidcCallbackUrl().href;
|
||||
const deviceId = credentials.deviceId;
|
||||
if (!deviceId) {
|
||||
throw new Error("Expected deviceId in user credentials.");
|
||||
}
|
||||
const tokenRefresher = new TokenRefresher(
|
||||
tokenIssuer,
|
||||
clientId,
|
||||
redirectUri,
|
||||
deviceId,
|
||||
idTokenClaims!,
|
||||
credentials.userId,
|
||||
);
|
||||
return tokenRefresher;
|
||||
}
|
||||
|
||||
/**
|
||||
* optionally clears localstorage, persists new credentials
|
||||
* to localstorage, starts the new client.
|
||||
@@ -869,21 +808,14 @@ async function doSetLoggedIn(
|
||||
await abortLogin();
|
||||
}
|
||||
|
||||
let storedClientid;
|
||||
let auth: OAuth2 | undefined;
|
||||
try {
|
||||
storedClientid = getStoredOidcClientId();
|
||||
auth = await hydrateAuth(credentials);
|
||||
} catch {}
|
||||
|
||||
let tokenRefresher;
|
||||
if (credentials.refreshToken && storedClientid) {
|
||||
tokenRefresher = await createOidcTokenRefresher(credentials, storedClientid);
|
||||
} else {
|
||||
logger.debug("No refresh token was supplied: access token will not be refreshed");
|
||||
}
|
||||
|
||||
// check the session lock just before creating the new client
|
||||
checkSessionLock();
|
||||
MatrixClientPeg.set(createClientWithCreds(credentials, tokenRefresher?.doRefreshAccessToken.bind(tokenRefresher)));
|
||||
MatrixClientPeg.set(createClientWithCreds(credentials, auth));
|
||||
const client = MatrixClientPeg.safeGet();
|
||||
|
||||
setSentryUser(credentials.userId);
|
||||
@@ -956,8 +888,7 @@ async function persistCredentials(credentials: IMatrixClientCreds): Promise<void
|
||||
localStorage.setItem("mx_user_id", credentials.userId);
|
||||
localStorage.setItem("mx_is_guest", JSON.stringify(credentials.guest));
|
||||
|
||||
await persistAccessTokenInStorage(credentials.accessToken, credentials.pickleKey);
|
||||
await persistRefreshTokenInStorage(credentials.refreshToken, credentials.pickleKey);
|
||||
await persistTokens(credentials.pickleKey, credentials);
|
||||
|
||||
if (credentials.pickleKey) {
|
||||
localStorage.setItem("mx_has_pickle_key", String(true));
|
||||
@@ -985,17 +916,25 @@ let _isLoggingOut = false;
|
||||
|
||||
/**
|
||||
* Logs out the current session.
|
||||
* When user has authenticated using OIDC native flow revoke tokens with OIDC provider.
|
||||
* When user has authenticated using OAuth2 native flow revoke tokens with OAuth2 provider.
|
||||
* Otherwise, call /logout on the homeserver.
|
||||
* @param client
|
||||
* @param oidcClientStore
|
||||
* @param oauth
|
||||
*/
|
||||
async function doLogout(client: MatrixClient, oidcClientStore?: OidcClientStore): Promise<void> {
|
||||
if (oidcClientStore?.isUserAuthenticatedWithOidc) {
|
||||
const accessToken = client.getAccessToken() ?? undefined;
|
||||
const refreshToken = client.getRefreshToken() ?? undefined;
|
||||
async function doLogout(client: MatrixClient, oauth: OAuth2 | null): Promise<void> {
|
||||
if (oauth) {
|
||||
const accessToken = client.getAccessToken();
|
||||
const refreshToken = client.getRefreshToken();
|
||||
|
||||
await oidcClientStore.revokeTokens(accessToken, refreshToken);
|
||||
await Promise.all(
|
||||
filterBoolean([
|
||||
accessToken ? oauth.revokeToken(accessToken, "access_token") : null,
|
||||
refreshToken ? oauth.revokeToken(refreshToken, "refresh_token") : null,
|
||||
]),
|
||||
);
|
||||
|
||||
client.stopClient();
|
||||
client.http.abort();
|
||||
} else {
|
||||
await client.logout(true);
|
||||
}
|
||||
@@ -1003,12 +942,21 @@ async function doLogout(client: MatrixClient, oidcClientStore?: OidcClientStore)
|
||||
|
||||
/**
|
||||
* Logs the current session out and transitions to the logged-out state
|
||||
* @param oidcClientStore store instance from SDKContext
|
||||
*/
|
||||
export function logout(oidcClientStore?: OidcClientStore): void {
|
||||
export async function logout(): Promise<void> {
|
||||
const client = MatrixClientPeg.get();
|
||||
if (!client) return;
|
||||
|
||||
let oauth: OAuth2 | undefined;
|
||||
try {
|
||||
oauth = await hydrateAuth({
|
||||
homeserverUrl: client.getHomeserverUrl(),
|
||||
deviceId: client.getDeviceId()!,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("@@", e);
|
||||
}
|
||||
|
||||
PosthogAnalytics.instance.logout();
|
||||
|
||||
if (client.isGuest()) {
|
||||
@@ -1022,7 +970,7 @@ export function logout(oidcClientStore?: OidcClientStore): void {
|
||||
_isLoggingOut = true;
|
||||
PlatformPeg.get()?.destroyPickleKey(client.getSafeUserId(), client.getDeviceId() ?? "");
|
||||
|
||||
doLogout(client, oidcClientStore).then(onLoggedOut, (err) => {
|
||||
doLogout(client, oauth ?? null).then(onLoggedOut, (err) => {
|
||||
// Just throwing an error here is going to be very unhelpful
|
||||
// if you're trying to log out because your server's down and
|
||||
// you want to log into a different server, so just forget the
|
||||
@@ -1269,3 +1217,18 @@ window.mxLoginWithAccessToken = async (hsUrl: string, accessToken: string): Prom
|
||||
false,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Instantiate an OAuth2 instance from storage
|
||||
* Returned promise will reject if the session or the server are not OAuth2-native.
|
||||
*/
|
||||
export async function hydrateAuth(
|
||||
credentials: Pick<IMatrixClientCreds, "homeserverUrl" | "deviceId">,
|
||||
): Promise<OAuth2> {
|
||||
const storedClientId = getStoredOAuthClientId();
|
||||
|
||||
const tempClient = new MatrixClient({ baseUrl: credentials.homeserverUrl });
|
||||
const authMetadata = await tempClient.getAuthMetadata();
|
||||
|
||||
return new OAuth2(authMetadata, { ...getOAuthParams(storedClientId), deviceId: credentials.deviceId });
|
||||
}
|
||||
|
||||
+34
-34
@@ -11,43 +11,43 @@ import {
|
||||
createClient,
|
||||
type MatrixClient,
|
||||
type LoginFlow,
|
||||
DELEGATED_OIDC_COMPATIBILITY,
|
||||
OAUTH_AWARE_PREFERRED_FLOW_FIELD,
|
||||
type ILoginFlow,
|
||||
type LoginRequest,
|
||||
type OidcClientConfig,
|
||||
type ValidatedAuthMetadata,
|
||||
type ISSOFlow,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { type IMatrixClientCreds } from "./utils/createMatrixClient";
|
||||
import { ModuleRunner } from "./modules/ModuleRunner";
|
||||
import { getOidcClientId } from "./utils/oidc/registerClient";
|
||||
import { getOAuthClientId } from "./utils/oauth/registerClient";
|
||||
import { type IConfigOptions } from "./IConfigOptions";
|
||||
import SdkConfig from "./SdkConfig";
|
||||
import { isUserRegistrationSupported } from "./utils/oidc/isUserRegistrationSupported";
|
||||
import { isUserRegistrationSupported } from "./utils/oauth/isUserRegistrationSupported";
|
||||
|
||||
/**
|
||||
* Login flows supported by this client
|
||||
* LoginFlow type use the client API /login endpoint
|
||||
* OidcNativeFlow is specific to this client
|
||||
* OAuthNativeFlow is specific to this client
|
||||
*/
|
||||
export type ClientLoginFlow = LoginFlow | OidcNativeFlow;
|
||||
export type ClientLoginFlow = LoginFlow | OAuthNativeFlow;
|
||||
|
||||
interface ILoginOptions {
|
||||
defaultDeviceDisplayName?: string;
|
||||
/**
|
||||
* Delegated auth config from server's .well-known.
|
||||
*
|
||||
* If this property is set, we will attempt an OIDC login using the delegated auth settings.
|
||||
* The caller is responsible for checking that OIDC is enabled in the labs settings.
|
||||
* If this property is set, we will attempt an OAuth2 login using the delegated auth settings.
|
||||
* The caller is responsible for checking that OAuth2 is enabled in the labs settings.
|
||||
*/
|
||||
delegatedAuthentication?: OidcClientConfig;
|
||||
delegatedAuthentication?: ValidatedAuthMetadata;
|
||||
}
|
||||
|
||||
export default class Login {
|
||||
private flows: Array<ClientLoginFlow> = [];
|
||||
private readonly defaultDeviceDisplayName?: string;
|
||||
private delegatedAuthentication?: OidcClientConfig;
|
||||
private delegatedAuthentication?: ValidatedAuthMetadata;
|
||||
private tempClient: MatrixClient | null = null; // memoize
|
||||
|
||||
public constructor(
|
||||
@@ -82,7 +82,7 @@ export default class Login {
|
||||
* Set delegated authentication config, clears tempClient.
|
||||
* @param delegatedAuthentication delegated auth config, from ValidatedServerConfig
|
||||
*/
|
||||
public setDelegatedAuthentication(delegatedAuthentication?: OidcClientConfig): void {
|
||||
public setDelegatedAuthentication(delegatedAuthentication?: ValidatedAuthMetadata): void {
|
||||
this.tempClient = null; // clear memoization
|
||||
this.delegatedAuthentication = delegatedAuthentication;
|
||||
}
|
||||
@@ -108,29 +108,29 @@ export default class Login {
|
||||
* @returns Promise that resolves to supported login flows
|
||||
*/
|
||||
public async getFlows(isRegistration?: boolean): Promise<Array<ClientLoginFlow>> {
|
||||
// try to use oidc native flow if we have delegated auth config
|
||||
// try to use oauth2 native flow if we have delegated auth config
|
||||
if (this.delegatedAuthentication) {
|
||||
try {
|
||||
const oidcFlow = await tryInitOidcNativeFlow(
|
||||
const oauthFlow = await tryInitOAuthNativeFlow(
|
||||
this.delegatedAuthentication,
|
||||
SdkConfig.get().oidc_static_clients,
|
||||
isRegistration,
|
||||
);
|
||||
return [oidcFlow];
|
||||
return [oauthFlow];
|
||||
} catch (error) {
|
||||
logger.error("Failed to get oidc native flow", error);
|
||||
logger.error("Failed to get OAuth2 native flow", error);
|
||||
}
|
||||
}
|
||||
|
||||
// oidc native flow not supported, continue with matrix login
|
||||
// OAuth2 native flow not supported, continue with matrix login
|
||||
const client = this.createTemporaryClient();
|
||||
const { flows }: { flows: LoginFlow[] } = await client.loginFlows();
|
||||
// If an m.login.sso flow is present which is also flagged as being for MSC3824 OIDC compatibility then we only
|
||||
// If an m.login.sso flow is present which is also flagged as being for MSC3824 OAuth compatibility then we only
|
||||
// return that flow as (per MSC3824) it is the only one that the user should be offered to give the best experience
|
||||
const oidcCompatibilityFlow = flows.find(
|
||||
(f) => f.type === "m.login.sso" && DELEGATED_OIDC_COMPATIBILITY.findIn(f as ISSOFlow),
|
||||
const oauthCompatibilityFlow = flows.find(
|
||||
(f) => f.type === "m.login.sso" && OAUTH_AWARE_PREFERRED_FLOW_FIELD.findIn(f as ISSOFlow),
|
||||
);
|
||||
this.flows = oidcCompatibilityFlow ? [oidcCompatibilityFlow] : flows;
|
||||
this.flows = oauthCompatibilityFlow ? [oauthCompatibilityFlow] : flows;
|
||||
return this.flows;
|
||||
}
|
||||
|
||||
@@ -199,42 +199,42 @@ export default class Login {
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes the OIDC native login flow
|
||||
* Describes the OAuth2 native login flow
|
||||
* Separate from js-sdk's `LoginFlow` as this does not use the same /login flow
|
||||
* to which that type belongs.
|
||||
*/
|
||||
export interface OidcNativeFlow extends ILoginFlow {
|
||||
type: "oidcNativeFlow";
|
||||
// this client's id as registered with the configured OIDC OP
|
||||
export interface OAuthNativeFlow extends ILoginFlow {
|
||||
type: "oauthNativeFlow";
|
||||
// this client's id as registered with the configured OAuth2 OP
|
||||
clientId: string;
|
||||
}
|
||||
/**
|
||||
* Prepares an OidcNativeFlow for logging into the server.
|
||||
* Prepares an OAuthNativeFlow for logging into the server.
|
||||
*
|
||||
* Finds a static clientId for configured issuer, or attempts dynamic registration with the OP, and wraps the
|
||||
* results.
|
||||
*
|
||||
* @param delegatedAuthConfig Auth config from ValidatedServerConfig
|
||||
* @param staticOidcClientIds static client config from config.json, used during client registration with OP
|
||||
* @param staticOAuthClientIds static client config from config.json, used during client registration with OP
|
||||
* @param isRegistration true when we are attempting registration
|
||||
* @returns Promise<OidcNativeFlow> when oidc native authentication flow is supported and correctly configured
|
||||
* @returns Promise<OAuthNativeFlow> when oauth native authentication flow is supported and correctly configured
|
||||
* @throws when client can't register with OP, or any unexpected error
|
||||
*/
|
||||
const tryInitOidcNativeFlow = async (
|
||||
delegatedAuthConfig: OidcClientConfig,
|
||||
staticOidcClientIds?: IConfigOptions["oidc_static_clients"],
|
||||
const tryInitOAuthNativeFlow = async (
|
||||
delegatedAuthConfig: ValidatedAuthMetadata,
|
||||
staticOAuthClientIds?: IConfigOptions["oidc_static_clients"],
|
||||
isRegistration?: boolean,
|
||||
): Promise<OidcNativeFlow> => {
|
||||
): Promise<OAuthNativeFlow> => {
|
||||
// if registration is not supported, bail before attempting to get the clientId
|
||||
if (isRegistration && !isUserRegistrationSupported(delegatedAuthConfig)) {
|
||||
throw new Error("Registration is not supported by OP");
|
||||
}
|
||||
const clientId = await getOidcClientId(delegatedAuthConfig, staticOidcClientIds);
|
||||
const clientId = await getOAuthClientId(delegatedAuthConfig, staticOAuthClientIds);
|
||||
|
||||
const flow = {
|
||||
type: "oidcNativeFlow",
|
||||
type: "oauthNativeFlow",
|
||||
clientId,
|
||||
} as OidcNativeFlow;
|
||||
} as OAuthNativeFlow;
|
||||
|
||||
return flow;
|
||||
};
|
||||
|
||||
@@ -340,11 +340,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
);
|
||||
|
||||
// remove the loginToken or auth code from the URL regardless
|
||||
if (
|
||||
!!this.props.urlParams.legacy_sso ||
|
||||
!!this.props.urlParams.oidc_fragment ||
|
||||
!!this.props.urlParams.oidc_query
|
||||
) {
|
||||
if (!!this.props.urlParams.legacy_sso || !!this.props.urlParams.oauth2) {
|
||||
this.props.onTokenLoginCompleted(this.props.urlParams, this.getFragmentAfterLogin());
|
||||
}
|
||||
|
||||
@@ -696,7 +692,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
case "logout":
|
||||
this.stores.legacyCallHandler.hangupAllCalls();
|
||||
Promise.all([...[...CallStore.instance.connectedCalls].map((call) => call.disconnect())]).finally(() =>
|
||||
Lifecycle.logout(this.stores.oidcClientStore),
|
||||
Lifecycle.logout(),
|
||||
);
|
||||
break;
|
||||
case "require_registration":
|
||||
|
||||
@@ -13,7 +13,7 @@ import { type SSOFlow, SSOAction } from "matrix-js-sdk/src/matrix";
|
||||
import { Button } from "@vector-im/compound-web";
|
||||
|
||||
import { _t, UserFriendlyError } from "../../../languageHandler";
|
||||
import Login, { type ClientLoginFlow, type OidcNativeFlow } from "../../../Login";
|
||||
import Login, { type ClientLoginFlow, type OAuthNativeFlow } from "../../../Login";
|
||||
import { messageForConnectionError, messageForLoginError } from "../../../utils/ErrorUtils";
|
||||
import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils";
|
||||
import AuthPage from "../../views/auth/AuthPage";
|
||||
@@ -31,7 +31,7 @@ import AuthHeader from "../../views/auth/AuthHeader";
|
||||
import AccessibleButton, { type ButtonEvent } from "../../views/elements/AccessibleButton";
|
||||
import { type ValidatedServerConfig } from "../../../utils/ValidatedServerConfig";
|
||||
import { filterBoolean } from "../../../utils/arrays";
|
||||
import { startOidcLogin } from "../../../utils/oidc/authorize";
|
||||
import { startOAuthLogin } from "../../../utils/oauth/authorize";
|
||||
import { ModuleApi } from "../../../modules/Api.ts";
|
||||
|
||||
interface IProps {
|
||||
@@ -122,7 +122,7 @@ class LoginComponent extends React.PureComponent<IProps, IState> {
|
||||
"m.login.cas": () => this.renderSsoStep("cas"),
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
"m.login.sso": () => this.renderSsoStep("sso"),
|
||||
"oidcNativeFlow": () => this.renderOidcNativeStep(),
|
||||
"oauthNativeFlow": () => this.renderOAuth2Step(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -402,7 +402,7 @@ class LoginComponent extends React.PureComponent<IProps, IState> {
|
||||
if (!this.state.flows) return null;
|
||||
|
||||
// this is the ideal order we want to show the flows in
|
||||
const order = ["oidcNativeFlow", "m.login.password", "m.login.sso"];
|
||||
const order = ["oauthNativeFlow", "m.login.password", "m.login.sso"];
|
||||
|
||||
const flows = filterBoolean(order.map((type) => this.state.flows?.find((flow) => flow.type === type)));
|
||||
return (
|
||||
@@ -434,15 +434,15 @@ class LoginComponent extends React.PureComponent<IProps, IState> {
|
||||
);
|
||||
};
|
||||
|
||||
private renderOidcNativeStep = (): React.ReactNode => {
|
||||
const flow = this.state.flows!.find((flow) => flow.type === "oidcNativeFlow")! as OidcNativeFlow;
|
||||
private renderOAuth2Step = (): React.ReactNode => {
|
||||
const flow = this.state.flows!.find((flow) => flow.type === "oauthNativeFlow")! as OAuthNativeFlow;
|
||||
return (
|
||||
<Button
|
||||
className="mx_Login_fullWidthButton"
|
||||
kind="primary"
|
||||
size="md"
|
||||
onClick={async () => {
|
||||
await startOidcLogin(
|
||||
await startOAuthLogin(
|
||||
this.props.serverConfig.delegatedAuthentication!,
|
||||
flow.clientId,
|
||||
this.props.serverConfig.hsUrl,
|
||||
|
||||
@@ -32,7 +32,7 @@ import * as Lifecycle from "../../../Lifecycle";
|
||||
import { type IMatrixClientCreds } from "../../../utils/createMatrixClient";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import AuthPage from "../../views/auth/AuthPage";
|
||||
import Login, { type OidcNativeFlow } from "../../../Login";
|
||||
import Login, { type OAuthNativeFlow } from "../../../Login";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import SSOButtons from "../../views/elements/SSOButtons";
|
||||
import ServerPicker from "../../views/elements/ServerPicker";
|
||||
@@ -46,7 +46,7 @@ import { AuthHeaderDisplay } from "./header/AuthHeaderDisplay";
|
||||
import { AuthHeaderProvider } from "./header/AuthHeaderProvider";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { type ValidatedServerConfig } from "../../../utils/ValidatedServerConfig";
|
||||
import { startOidcLogin } from "../../../utils/oidc/authorize";
|
||||
import { startOAuthLogin } from "../../../utils/oauth/authorize";
|
||||
|
||||
const debuglog = (...args: any[]): void => {
|
||||
if (SettingsStore.getValue("debug_registration")) {
|
||||
@@ -124,7 +124,7 @@ interface IState {
|
||||
ssoFlow?: SSOFlow;
|
||||
// the OIDC native login flow, when supported and enabled
|
||||
// if present, must be used for registration
|
||||
oidcNativeFlow?: OidcNativeFlow;
|
||||
oauthNativeFlow?: OAuthNativeFlow;
|
||||
}
|
||||
|
||||
export default class Registration extends React.Component<IProps, IState> {
|
||||
@@ -225,12 +225,12 @@ export default class Registration extends React.Component<IProps, IState> {
|
||||
this.loginLogic.setDelegatedAuthentication(serverConfig.delegatedAuthentication);
|
||||
|
||||
let ssoFlow: SSOFlow | undefined;
|
||||
let oidcNativeFlow: OidcNativeFlow | undefined;
|
||||
let oauthNativeFlow: OAuthNativeFlow | undefined;
|
||||
try {
|
||||
const loginFlows = await this.loginLogic.getFlows(true);
|
||||
if (serverConfig !== this.latestServerConfig) return; // discard, serverConfig changed from under us
|
||||
ssoFlow = loginFlows.find((f) => f.type === "m.login.sso" || f.type === "m.login.cas") as SSOFlow;
|
||||
oidcNativeFlow = loginFlows.find((f) => f.type === "oidcNativeFlow") as OidcNativeFlow;
|
||||
oauthNativeFlow = loginFlows.find((f) => f.type === "oauthNativeFlow") as OAuthNativeFlow;
|
||||
} catch (e) {
|
||||
if (serverConfig !== this.latestServerConfig) return; // discard, serverConfig changed from under us
|
||||
logger.error("Failed to get login flows to check for SSO support", e);
|
||||
@@ -241,10 +241,10 @@ export default class Registration extends React.Component<IProps, IState> {
|
||||
({ flows }) => ({
|
||||
matrixClient: cli,
|
||||
ssoFlow,
|
||||
oidcNativeFlow,
|
||||
oauthNativeFlow,
|
||||
// if we are using oidc native we won't continue with flow discovery on HS
|
||||
// so set an empty array to indicate flows are no longer loading
|
||||
flows: oidcNativeFlow ? [] : flows,
|
||||
flows: oauthNativeFlow ? [] : flows,
|
||||
busy: false,
|
||||
}),
|
||||
resolve,
|
||||
@@ -253,7 +253,7 @@ export default class Registration extends React.Component<IProps, IState> {
|
||||
|
||||
// don't need to check with homeserver for login flows
|
||||
// since we are going to use OIDC native flow
|
||||
if (oidcNativeFlow) {
|
||||
if (oauthNativeFlow) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -546,16 +546,16 @@ export default class Registration extends React.Component<IProps, IState> {
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
} else if (this.state.matrixClient && this.state.oidcNativeFlow) {
|
||||
} else if (this.state.matrixClient && this.state.oauthNativeFlow) {
|
||||
return (
|
||||
<Button
|
||||
className="mx_Login_fullWidthButton"
|
||||
kind="primary"
|
||||
size="md"
|
||||
onClick={async () => {
|
||||
await startOidcLogin(
|
||||
await startOAuthLogin(
|
||||
this.props.serverConfig.delegatedAuthentication!,
|
||||
this.state.oidcNativeFlow!.clientId,
|
||||
this.state.oauthNativeFlow!.clientId,
|
||||
this.props.serverConfig.hsUrl,
|
||||
this.props.serverConfig.isUrl,
|
||||
true /* isRegistration */,
|
||||
|
||||
@@ -26,7 +26,6 @@ import AccessibleButton from "../../views/elements/AccessibleButton";
|
||||
import Spinner from "../../views/elements/Spinner";
|
||||
import AuthHeader from "../../views/auth/AuthHeader";
|
||||
import AuthBody from "../../views/auth/AuthBody";
|
||||
import { SDKContext } from "../../../contexts/SDKContext";
|
||||
import { type URLParams } from "../../../vector/url_utils.ts";
|
||||
|
||||
enum LoginView {
|
||||
@@ -61,9 +60,6 @@ interface IState {
|
||||
}
|
||||
|
||||
export default class SoftLogout extends React.Component<IProps, IState> {
|
||||
public static contextType = SDKContext;
|
||||
declare public context: React.ContextType<typeof SDKContext>;
|
||||
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
@@ -92,7 +88,7 @@ export default class SoftLogout extends React.Component<IProps, IState> {
|
||||
if (!wipeData) return;
|
||||
|
||||
logger.log("Clearing data from soft-logged-out session");
|
||||
Lifecycle.logout(this.context.oidcClientStore);
|
||||
Lifecycle.logout();
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -18,17 +18,24 @@ import {
|
||||
signInByGeneratingQR,
|
||||
} from "matrix-js-sdk/src/rendezvous";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { AutoDiscovery, MatrixClient, OAuthGrantType, type OidcClientConfig, type XOR } from "matrix-js-sdk/src/matrix";
|
||||
import {
|
||||
AutoDiscovery,
|
||||
MatrixClient,
|
||||
OAuthGrantType,
|
||||
type ValidatedAuthMetadata,
|
||||
type XOR,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { sleep } from "matrix-js-sdk/src/utils";
|
||||
import { secureRandomString } from "matrix-js-sdk/src/randomstring";
|
||||
|
||||
import { Click, Mode, Phase } from "./LoginWithQR-types";
|
||||
import LoginWithQRFlow from "./LoginWithQRFlow";
|
||||
import { type CompleteOidcLoginResponse } from "../../../utils/oidc/authorize";
|
||||
import { getOidcClientId } from "../../../utils/oidc/registerClient.ts";
|
||||
import { type CompleteOAuthLoginResponse } from "../../../utils/oauth/authorize";
|
||||
import { getOAuthClientId } from "../../../utils/oauth/registerClient.ts";
|
||||
import SdkConfig from "../../../SdkConfig.ts";
|
||||
import { type Context } from "../../../utils/oauth/persistOAuthSettings.ts";
|
||||
|
||||
export type QrLoginCredentials = Omit<CompleteOidcLoginResponse, "idTokenClaims"> &
|
||||
export type QrLoginCredentials = CompleteOAuthLoginResponse &
|
||||
Awaited<ReturnType<MSC4108SignInWithQR["shareSecrets"]>> & {
|
||||
deviceId: string;
|
||||
};
|
||||
@@ -101,26 +108,10 @@ interface IState {
|
||||
*/
|
||||
failureReason?: FailureReason;
|
||||
/**
|
||||
* TODO
|
||||
* Details of the server we are logging into, set after initial protocol negotiation.
|
||||
*/
|
||||
loginServerDetails?: {
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
homeserverUrl: string;
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
identityServerUrl?: string;
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
metadata: OidcClientConfig;
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
clientId: string;
|
||||
};
|
||||
loginServerDetails?: Pick<Context, "homeserverUrl" | "identityServerUrl" | "metadata"> &
|
||||
Pick<Context["authContext"], "clientId">;
|
||||
}
|
||||
|
||||
export enum LoginWithQRFailureReason {
|
||||
@@ -141,7 +132,7 @@ export type FailureReason = RendezvousFailureReason | LoginWithQRFailureReason;
|
||||
*/
|
||||
async function resolveServerURLs(
|
||||
serverNameOrBaseUrl: string,
|
||||
): Promise<Pick<Partial<NonNullable<IState["loginServerDetails"]>>, "homeserverUrl" | "identityServerUrl">> {
|
||||
): Promise<Pick<Partial<Context>, "homeserverUrl" | "identityServerUrl">> {
|
||||
if (serverNameOrBaseUrl.startsWith("http://") || serverNameOrBaseUrl.startsWith("https://")) {
|
||||
// treat as base URL and skip discovery
|
||||
return {
|
||||
@@ -262,7 +253,7 @@ export default class LoginWithQR extends React.Component<Props, IState> {
|
||||
throw new Error("Failed to discover homeserver URL");
|
||||
}
|
||||
|
||||
let metadata: OidcClientConfig;
|
||||
let metadata: ValidatedAuthMetadata;
|
||||
let clientId: string;
|
||||
try {
|
||||
// Create a new client as the homeserver URL may not be the same as we used for the secure channel
|
||||
@@ -270,7 +261,7 @@ export default class LoginWithQR extends React.Component<Props, IState> {
|
||||
if (!metadata.grant_types_supported.includes(OAuthGrantType.DeviceAuthorization)) {
|
||||
throw new Error("Server does not support Device Authorization Grant");
|
||||
}
|
||||
clientId = await getOidcClientId(metadata, SdkConfig.get().oidc_static_clients);
|
||||
clientId = await getOAuthClientId(metadata, SdkConfig.get().oidc_static_clients);
|
||||
} catch (e) {
|
||||
this.setState({
|
||||
phase: Phase.Error,
|
||||
@@ -355,8 +346,6 @@ export default class LoginWithQR extends React.Component<Props, IState> {
|
||||
refreshToken: tokenResponse.refresh_token,
|
||||
homeserverUrl,
|
||||
clientId,
|
||||
idToken: tokenResponse.id_token,
|
||||
issuer: metadata!.issuer,
|
||||
identityServerUrl,
|
||||
secrets,
|
||||
deviceId,
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
type SSOFlow,
|
||||
type SSOAction,
|
||||
type IIdentityProvider,
|
||||
DELEGATED_OIDC_COMPATIBILITY,
|
||||
OAUTH_AWARE_PREFERRED_FLOW_FIELD,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { type Signup } from "@matrix-org/analytics-events/types/typescript/Signup";
|
||||
import { Button, Tooltip } from "@vector-im/compound-web";
|
||||
@@ -90,7 +90,7 @@ const SSOButton: React.FC<ISSOButtonProps> = ({
|
||||
let label: string;
|
||||
if (idp) {
|
||||
label = _t("auth|continue_with_idp", { provider: idp.name });
|
||||
} else if (DELEGATED_OIDC_COMPATIBILITY.findIn<boolean>(flow)) {
|
||||
} else if (OAUTH_AWARE_PREFERRED_FLOW_FIELD.findIn<boolean>(flow)) {
|
||||
label = _t("action|continue");
|
||||
} else {
|
||||
label = _t("auth|sign_in_with_sso");
|
||||
|
||||
@@ -18,7 +18,7 @@ import ToggleSwitch from "../../elements/ToggleSwitch";
|
||||
import { DeviceDetailHeading } from "./DeviceDetailHeading";
|
||||
import { DeviceVerificationStatusCard } from "./DeviceVerificationStatusCard";
|
||||
import { type ExtendedDevice } from "./types";
|
||||
import { getManageDeviceUrl } from "../../../../utils/oidc/urls.ts";
|
||||
import { getManageDeviceUrl } from "../../../../utils/oauth/urls.ts";
|
||||
|
||||
interface Props {
|
||||
device: ExtendedDevice;
|
||||
|
||||
@@ -23,9 +23,8 @@ import ChangePassword from "../../ChangePassword";
|
||||
import SettingsTab from "../SettingsTab";
|
||||
import { SettingsSection } from "../../shared/SettingsSection";
|
||||
import { SettingsSubsection, SettingsSubsectionText } from "../../shared/SettingsSubsection";
|
||||
import { SDKContext } from "../../../../../contexts/SDKContext";
|
||||
import { UserPersonalInfoSettings } from "../../UserPersonalInfoSettings";
|
||||
import { useMatrixClientContext } from "../../../../../contexts/MatrixClientContext";
|
||||
import { SDKContext } from "../../../../../contexts/SDKContext.ts";
|
||||
|
||||
interface IProps {
|
||||
closeSettingsFn: () => void;
|
||||
@@ -90,8 +89,8 @@ const AccountUserSettingsTab: React.FC<IProps> = ({ closeSettingsFn }) => {
|
||||
const [canSetAvatar, setCanSetAvatar] = React.useState<boolean>(false);
|
||||
const [canChangePassword, setCanChangePassword] = React.useState<boolean>(false);
|
||||
|
||||
const cli = useMatrixClientContext();
|
||||
const sdkContext = useContext(SDKContext);
|
||||
const cli = sdkContext.client!;
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
@@ -103,8 +102,8 @@ const AccountUserSettingsTab: React.FC<IProps> = ({ closeSettingsFn }) => {
|
||||
// the enabled flag value.
|
||||
const canChangePassword = !changePasswordCap || changePasswordCap["enabled"] !== false;
|
||||
|
||||
await sdkContext.oidcClientStore.readyPromise; // wait for the store to be ready
|
||||
const externalAccountManagementUrl = sdkContext.oidcClientStore.accountManagementEndpoint;
|
||||
const authMetadata = await cli.getAuthMetadata().catch(() => {});
|
||||
const externalAccountManagementUrl = authMetadata?.account_management_uri;
|
||||
// https://spec.matrix.org/v1.7/client-server-api/#m3pid_changes-capability
|
||||
// We support as far back as v1.1 which doesn't have m.3pid_changes
|
||||
// so the behaviour for when it is missing has to be assume true
|
||||
@@ -121,7 +120,7 @@ const AccountUserSettingsTab: React.FC<IProps> = ({ closeSettingsFn }) => {
|
||||
setExternalAccountManagementUrl(externalAccountManagementUrl);
|
||||
setCanChangePassword(canChangePassword);
|
||||
})();
|
||||
}, [cli, sdkContext.oidcClientStore]);
|
||||
}, [cli]);
|
||||
|
||||
const onPasswordChangeError = useCallback((err: Error): void => {
|
||||
logger.error("Failed to change password: " + err);
|
||||
|
||||
@@ -31,7 +31,7 @@ import QuestionDialog from "../../../dialogs/QuestionDialog";
|
||||
import { type FilterVariation } from "../../devices/filter";
|
||||
import { OtherSessionsSectionHeading } from "../../devices/OtherSessionsSectionHeading";
|
||||
import { SettingsSection } from "../../shared/SettingsSection";
|
||||
import { getManageDeviceUrl } from "../../../../../utils/oidc/urls.ts";
|
||||
import { getManageDeviceUrl } from "../../../../../utils/oauth/urls.ts";
|
||||
import { SDKContext } from "../../../../../contexts/SDKContext";
|
||||
import Spinner from "../../../elements/Spinner";
|
||||
|
||||
@@ -154,12 +154,12 @@ const SessionManagerTab: React.FC<{
|
||||
* See https://github.com/matrix-org/matrix-spec-proposals/pull/3824
|
||||
*/
|
||||
const accountManagement = useAsyncMemo(async () => {
|
||||
await sdkContext.oidcClientStore.readyPromise; // wait for the store to be ready
|
||||
const authMetadata = await matrixClient.getAuthMetadata().catch(() => {});
|
||||
return {
|
||||
endpoint: sdkContext.oidcClientStore.accountManagementEndpoint,
|
||||
actionsSupported: sdkContext.oidcClientStore.accountManagementActionsSupported,
|
||||
endpoint: authMetadata?.account_management_uri,
|
||||
actionsSupported: authMetadata?.account_management_actions_supported,
|
||||
};
|
||||
}, [sdkContext.oidcClientStore]);
|
||||
}, [matrixClient]);
|
||||
const disableMultipleSignout = !!accountManagement?.endpoint;
|
||||
const userId = matrixClient?.getUserId();
|
||||
const currentUserMember = (userId && matrixClient?.getUser(userId)) || undefined;
|
||||
|
||||
@@ -82,7 +82,6 @@ import { KeyboardShortcut } from "../settings/KeyboardShortcut";
|
||||
import { ModuleApi } from "../../../modules/Api.ts";
|
||||
import { useModuleSpacePanelItems } from "../../../modules/ExtrasApi.ts";
|
||||
import { UserMenuViewModel } from "../../../viewmodels/menus/UserMenuViewModel.ts";
|
||||
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext.tsx";
|
||||
import { SDKContext } from "../../../contexts/SDKContext.ts";
|
||||
import { OwnProfileStore } from "../../../stores/OwnProfileStore.ts";
|
||||
|
||||
@@ -390,7 +389,8 @@ const InnerSpacePanel = React.memo<IInnerSpacePanelProps>(
|
||||
);
|
||||
|
||||
const SpacePanel: React.FC = () => {
|
||||
const client = useMatrixClientContext();
|
||||
const sdkContext = useContext(SDKContext);
|
||||
const client = sdkContext.client!;
|
||||
const [dragging, setDragging] = useState(false);
|
||||
const [isPanelCollapsed, setPanelCollapsed] = useState(true);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
@@ -398,7 +398,6 @@ const SpacePanel: React.FC = () => {
|
||||
if (ref.current) UIStore.instance.trackElementDimensions("SpacePanel", ref.current);
|
||||
return () => UIStore.instance.stopTrackingElementDimensions("SpacePanel");
|
||||
}, []);
|
||||
const sdkContext = useContext(SDKContext);
|
||||
|
||||
useDispatcher(defaultDispatcher, (payload: ActionPayload) => {
|
||||
if (payload.action === Action.ToggleSpacePanel) {
|
||||
@@ -413,7 +412,6 @@ const SpacePanel: React.FC = () => {
|
||||
defaultDispatcher,
|
||||
client,
|
||||
isPanelCollapsed,
|
||||
sdkContext.oidcClientStore.accountManagementEndpoint,
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ import TypingStore from "../stores/TypingStore";
|
||||
import { UserProfilesStore } from "../stores/UserProfilesStore";
|
||||
import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore";
|
||||
import { WidgetPermissionStore } from "../stores/widgets/WidgetPermissionStore";
|
||||
import { OidcClientStore } from "../stores/oidc/OidcClientStore";
|
||||
import WidgetStore from "../stores/WidgetStore";
|
||||
import ResizeNotifier from "../utils/ResizeNotifier";
|
||||
import { MultiRoomViewStore } from "../stores/MultiRoomViewStore";
|
||||
@@ -70,7 +69,6 @@ export class SDKContextClass {
|
||||
protected _LegacyCallHandler?: LegacyCallHandler;
|
||||
protected _TypingStore?: TypingStore;
|
||||
protected _UserProfilesStore?: UserProfilesStore;
|
||||
protected _OidcClientStore?: OidcClientStore;
|
||||
protected _ResizeNotifier?: ResizeNotifier;
|
||||
protected _MultiRoomViewStore?: MultiRoomViewStore;
|
||||
protected _Notifier?: Notifier;
|
||||
@@ -181,18 +179,6 @@ export class SDKContextClass {
|
||||
return this._UserProfilesStore;
|
||||
}
|
||||
|
||||
public get oidcClientStore(): OidcClientStore {
|
||||
if (!this.client) {
|
||||
throw new Error("Unable to create OidcClientStore without a client");
|
||||
}
|
||||
|
||||
if (!this._OidcClientStore) {
|
||||
this._OidcClientStore = new OidcClientStore(this.client);
|
||||
}
|
||||
|
||||
return this._OidcClientStore;
|
||||
}
|
||||
|
||||
// This is getting increasingly tenuous to have here but we still have class components so it's
|
||||
// awkward to consume multiple contexts in them. This should be replaced with ResizeObservers
|
||||
// anyway really.
|
||||
@@ -219,7 +205,6 @@ export class SDKContextClass {
|
||||
|
||||
public onLoggedOut(): void {
|
||||
this._UserProfilesStore = undefined;
|
||||
this._OidcClientStore = undefined;
|
||||
this._client = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,183 +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 { type MatrixClient, discoverAndValidateOIDCIssuerWellKnown } from "matrix-js-sdk/src/matrix";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { OidcClient } from "oidc-client-ts";
|
||||
|
||||
import {
|
||||
getStoredOidcTokenIssuer,
|
||||
getStoredOidcClientId,
|
||||
getStoredOidcIdToken,
|
||||
} from "../../utils/oidc/persistOidcSettings";
|
||||
import PlatformPeg from "../../PlatformPeg";
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* Stores information about configured OIDC provider
|
||||
*
|
||||
* In OIDC Native mode the client is registered with OIDC directly and maintains an OIDC token.
|
||||
*
|
||||
* In OIDC Aware mode, the client is aware that the Server is using OIDC, but is using the standard Matrix APIs for most things.
|
||||
* (Notable exceptions are account management, where a link to the account management endpoint will be provided instead.)
|
||||
*
|
||||
* Otherwise, the store is not operating. Auth is then in Legacy mode and everything uses normal Matrix APIs.
|
||||
*/
|
||||
export class OidcClientStore {
|
||||
private oidcClient?: OidcClient;
|
||||
private initialisingOidcClientPromise: Promise<void> | undefined;
|
||||
private authenticatedIssuer?: string; // set only in OIDC-native mode
|
||||
private _accountManagementEndpoint?: string;
|
||||
private _accountManagementActionsSupported?: string[];
|
||||
/**
|
||||
* Promise which resolves once this store is read to use, which may mean there is no OIDC client if we're in legacy mode,
|
||||
* or we just have the account management endpoint if running in OIDC-aware mode.
|
||||
*/
|
||||
public readonly readyPromise: Promise<void>;
|
||||
|
||||
public constructor(private readonly matrixClient: MatrixClient) {
|
||||
this.readyPromise = this.init();
|
||||
}
|
||||
|
||||
private async init(): Promise<void> {
|
||||
this.authenticatedIssuer = getStoredOidcTokenIssuer();
|
||||
if (this.authenticatedIssuer) {
|
||||
await this.getOidcClient();
|
||||
} else {
|
||||
// We are not in OIDC Native mode, as we have no locally stored issuer. Check if the server delegates auth to OIDC.
|
||||
try {
|
||||
const authMetadata = await this.matrixClient.getAuthMetadata();
|
||||
this.setAccountManagementEndpoint(
|
||||
authMetadata.account_management_uri,
|
||||
authMetadata.issuer,
|
||||
authMetadata.account_management_actions_supported,
|
||||
);
|
||||
} catch (e) {
|
||||
console.log("Auth issuer not found", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* True when the active user is authenticated via OIDC
|
||||
*/
|
||||
public get isUserAuthenticatedWithOidc(): boolean {
|
||||
return !!this.authenticatedIssuer;
|
||||
}
|
||||
|
||||
private setAccountManagementEndpoint(
|
||||
endpoint: string | undefined,
|
||||
issuer: string,
|
||||
actionsSupported?: string[],
|
||||
): void {
|
||||
// if no account endpoint is configured default to the issuer
|
||||
const url = new URL(endpoint ?? issuer);
|
||||
const idToken = getStoredOidcIdToken();
|
||||
if (idToken) {
|
||||
url.searchParams.set("id_token_hint", idToken);
|
||||
}
|
||||
this._accountManagementEndpoint = url.toString();
|
||||
this._accountManagementActionsSupported = actionsSupported;
|
||||
}
|
||||
|
||||
public get accountManagementEndpoint(): string | undefined {
|
||||
return this._accountManagementEndpoint;
|
||||
}
|
||||
|
||||
public get accountManagementActionsSupported(): string[] | undefined {
|
||||
return this._accountManagementActionsSupported;
|
||||
}
|
||||
|
||||
/**
|
||||
* Revokes provided access and refresh tokens with the configured OIDC provider
|
||||
* @param accessToken
|
||||
* @param refreshToken
|
||||
* @returns Promise that resolves when tokens have been revoked
|
||||
* @throws when OidcClient cannot be initialised, or revoking either token fails
|
||||
*/
|
||||
public async revokeTokens(accessToken?: string, refreshToken?: string): Promise<void> {
|
||||
const client = await this.getOidcClient();
|
||||
|
||||
if (!client) {
|
||||
throw new Error("No OIDC client");
|
||||
}
|
||||
|
||||
const results = await Promise.all([
|
||||
this.tryRevokeToken(client, accessToken, "access_token"),
|
||||
this.tryRevokeToken(client, refreshToken, "refresh_token"),
|
||||
]);
|
||||
|
||||
if (results.some((success) => !success)) {
|
||||
throw new Error("Failed to revoke tokens");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to revoke a given token
|
||||
* @param oidcClient
|
||||
* @param token
|
||||
* @param tokenType passed to revocation endpoint as token type hint
|
||||
* @returns Promise that resolved with boolean whether the token revocation succeeded or not
|
||||
*/
|
||||
private async tryRevokeToken(
|
||||
oidcClient: OidcClient,
|
||||
token: string | undefined,
|
||||
tokenType: "access_token" | "refresh_token",
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
if (!token) {
|
||||
return false;
|
||||
}
|
||||
await oidcClient.revokeToken(token, tokenType);
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error(`Failed to revoke ${tokenType}`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async getOidcClient(): Promise<OidcClient | undefined> {
|
||||
if (!this.oidcClient && !this.initialisingOidcClientPromise) {
|
||||
this.initialisingOidcClientPromise = this.initOidcClient();
|
||||
}
|
||||
await this.initialisingOidcClientPromise;
|
||||
this.initialisingOidcClientPromise = undefined;
|
||||
return this.oidcClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to initialise an OidcClient using stored clientId and OIDC discovery.
|
||||
* Assigns this.oidcClient and accountManagement endpoint.
|
||||
* Logs errors and does not throw when oidc client cannot be initialised.
|
||||
* @returns promise that resolves when initialising OidcClient succeeds or fails
|
||||
*/
|
||||
private async initOidcClient(): Promise<void> {
|
||||
if (!this.authenticatedIssuer) {
|
||||
logger.error("Cannot initialise OIDC client without issuer.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const clientId = getStoredOidcClientId();
|
||||
const authMetadata = await discoverAndValidateOIDCIssuerWellKnown(this.authenticatedIssuer);
|
||||
this.setAccountManagementEndpoint(
|
||||
authMetadata.account_management_uri,
|
||||
authMetadata.issuer,
|
||||
authMetadata.account_management_actions_supported,
|
||||
);
|
||||
this.oidcClient = new OidcClient({
|
||||
authority: authMetadata.issuer,
|
||||
signingKeys: authMetadata.signingKeys ?? undefined,
|
||||
redirect_uri: PlatformPeg.get()!.getOidcCallbackUrl().href,
|
||||
client_id: clientId,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Failed to initialise OidcClientStore", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
type IClientWellKnown,
|
||||
MatrixClient,
|
||||
MatrixError,
|
||||
type OidcClientConfig,
|
||||
type ValidatedAuthMetadata,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
@@ -288,7 +288,7 @@ export default class AutoDiscoveryUtils {
|
||||
|
||||
// This isn't inherently auto-discovery but used to be in an earlier incarnation of the MSC,
|
||||
// and shuttling the data together makes a lot of sense
|
||||
let delegatedAuthentication: OidcClientConfig | undefined;
|
||||
let delegatedAuthentication: ValidatedAuthMetadata | undefined;
|
||||
let delegatedAuthenticationError: Error | undefined;
|
||||
try {
|
||||
const tempClient = new MatrixClient({ baseUrl: preferredHomeserverUrl });
|
||||
|
||||
@@ -6,7 +6,7 @@ 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 { type OidcClientConfig } from "matrix-js-sdk/src/matrix";
|
||||
import { type ValidatedAuthMetadata } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
export interface ValidatedServerConfig {
|
||||
hsUrl: string;
|
||||
@@ -24,8 +24,8 @@ export interface ValidatedServerConfig {
|
||||
/**
|
||||
* Config related to delegated authentication
|
||||
* Included when delegated auth is configured and valid, otherwise undefined.
|
||||
* From issuer's .well-known/openid-configuration.
|
||||
* From {@link MatrixClient.getAuthMetadata}.
|
||||
* Used for OIDC native flow authentication.
|
||||
*/
|
||||
delegatedAuthentication?: OidcClientConfig;
|
||||
delegatedAuthentication?: ValidatedAuthMetadata;
|
||||
}
|
||||
|
||||
@@ -17,11 +17,13 @@ import {
|
||||
LocalStorageCryptoStore,
|
||||
RoomNameType,
|
||||
type RoomNameState,
|
||||
type TokenRefreshFunction,
|
||||
EventTimelineSet,
|
||||
EventTimeline,
|
||||
type OAuth2,
|
||||
TokenRefresher,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { VerificationMethod } from "matrix-js-sdk/src/types";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import indexeddbWorkerFactory from "../workers/indexeddbWorkerFactory";
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
@@ -29,6 +31,7 @@ import { crossSigningCallbacks } from "../SecurityManager";
|
||||
import IdentityAuthClient from "../IdentityAuthClient";
|
||||
import { _t } from "../languageHandler";
|
||||
import { formatList } from "./FormattingUtils";
|
||||
import { persistTokens } from "./tokens/tokens.ts";
|
||||
|
||||
const localStorage = window.localStorage;
|
||||
|
||||
@@ -116,14 +119,19 @@ function roomNameGenerator(_: string, state: RoomNameState): string | null {
|
||||
* Create a new matrix client from credentials with all the options needed.
|
||||
*
|
||||
* @param creds The credentials to create the client with
|
||||
* @param tokenRefreshFunction Optional function to call when the access token is expired
|
||||
* @param oauth The OAuth2 instance for OAuth2-native sessions
|
||||
*
|
||||
* @returns {MatrixClient} the newly-created MatrixClient
|
||||
*/
|
||||
export function createClientWithCreds(
|
||||
creds: IMatrixClientCreds,
|
||||
tokenRefreshFunction?: TokenRefreshFunction,
|
||||
): MatrixClient {
|
||||
export function createClientWithCreds(creds: IMatrixClientCreds, oauth?: OAuth2): MatrixClient {
|
||||
let tokenRefreshFunction: ICreateClientOpts["tokenRefreshFunction"];
|
||||
if (creds.refreshToken && oauth) {
|
||||
const tokenRefresher = new TokenRefresher(oauth, persistTokens.bind(null, creds.pickleKey));
|
||||
tokenRefreshFunction = tokenRefresher?.tokenRefreshFunction;
|
||||
} else {
|
||||
logger.debug("No refresh token was supplied: access token will not be refreshed");
|
||||
}
|
||||
|
||||
const opts: ICreateClientOpts = {
|
||||
baseUrl: creds.homeserverUrl,
|
||||
idBaseUrl: creds.identityServerUrl,
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
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 { OAuth2, OAuth2Error, type ValidatedAuthMetadata } from "matrix-js-sdk/src/matrix";
|
||||
import { secureRandomString } from "matrix-js-sdk/src/randomstring";
|
||||
|
||||
import { OAuthClientError } from "./error";
|
||||
import PlatformPeg from "../../PlatformPeg";
|
||||
import { type URLParams } from "../../vector/url_utils.ts";
|
||||
import { getOAuthParams, loadAuthContext, storeAuthContext } from "./persistOAuthSettings.ts";
|
||||
|
||||
const RESPONSE_MODE = "fragment";
|
||||
|
||||
/**
|
||||
* Start OAuth2 authorization code flow
|
||||
* Generates auth params, stores them in session storage and
|
||||
* Navigates to configured authorization endpoint
|
||||
* @param authMetadata from {@link MatrixClient.getAuthMetdata}
|
||||
* @param clientId this client's id as registered with configured issuer
|
||||
* @param homeserverUrl target homeserver
|
||||
* @param identityServerUrl OPTIONAL target identity server
|
||||
* @param isRegistration if true will set the prompt to "create"
|
||||
* @returns Promise that resolves after we have navigated to auth endpoint
|
||||
*/
|
||||
export const startOAuthLogin = async (
|
||||
authMetadata: ValidatedAuthMetadata,
|
||||
clientId: string,
|
||||
homeserverUrl: string,
|
||||
identityServerUrl?: string,
|
||||
isRegistration?: boolean,
|
||||
): Promise<void> => {
|
||||
const platform = PlatformPeg.get()!;
|
||||
const state = secureRandomString(32) + platform.getOAuthClientState();
|
||||
|
||||
const auth = new OAuth2(authMetadata, getOAuthParams(clientId));
|
||||
storeAuthContext({
|
||||
authContext: auth.context,
|
||||
metadata: authMetadata,
|
||||
homeserverUrl,
|
||||
identityServerUrl,
|
||||
state,
|
||||
});
|
||||
|
||||
const authorizationUrl = await auth.generateAuthorizationCodeGrantUrl(
|
||||
state,
|
||||
RESPONSE_MODE,
|
||||
isRegistration ? "create" : undefined,
|
||||
);
|
||||
|
||||
window.location.href = authorizationUrl;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets `code` and `state` response params
|
||||
*
|
||||
* @param urlParams - the parameters to read
|
||||
* @returns code and state
|
||||
* @throws when code and state are not valid strings
|
||||
*/
|
||||
const getCodeAndStateFromParams = ({
|
||||
code,
|
||||
state,
|
||||
}: NonNullable<URLParams["oauth2"]>): { code: string; state: string } => {
|
||||
if (!code || typeof code !== "string" || !state || typeof state !== "string") {
|
||||
throw new Error(OAuthClientError.InvalidFragmentParameters);
|
||||
}
|
||||
return { code, state };
|
||||
};
|
||||
|
||||
/**
|
||||
* Return type for {@link completeOAuthLogin}
|
||||
* Contains all the credentials gathered from a successful OIDC login
|
||||
*/
|
||||
export type CompleteOAuthLoginResponse = {
|
||||
/**
|
||||
* URL of the homeserver selected during login
|
||||
*/
|
||||
homeserverUrl: string;
|
||||
/**
|
||||
* Identity server URL as discovered during login
|
||||
*/
|
||||
identityServerUrl?: string;
|
||||
/**
|
||||
* Access Token gained from OIDC token issuer
|
||||
*/
|
||||
accessToken: string;
|
||||
/**
|
||||
* Refresh Token gained from OIDC token issuer, when falsy token cannot be refreshed
|
||||
*/
|
||||
refreshToken?: string;
|
||||
/**
|
||||
* This client's ID as registered with the OIDC issuer
|
||||
*/
|
||||
clientId: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Attempt to complete authorization code flow to get an access token
|
||||
* @param urlParams the parameters extracted from the app-load URI.
|
||||
* @returns Promise that resolves with a CompleteOAuthLoginResponse when login was successful
|
||||
* @throws When we failed to get a valid access token
|
||||
*/
|
||||
export const completeOAuthLogin = async (
|
||||
urlParams: NonNullable<URLParams["oauth2"]>,
|
||||
): Promise<CompleteOAuthLoginResponse> => {
|
||||
const { code, state } = getCodeAndStateFromParams(urlParams);
|
||||
|
||||
const context = loadAuthContext();
|
||||
if (context?.state !== state) {
|
||||
throw new Error(OAuth2Error.MissingOrInvalidStoredState);
|
||||
}
|
||||
|
||||
const bearerToken = await new OAuth2(context.metadata, context.authContext).completeAuthorizationCodeGrant(code);
|
||||
|
||||
return {
|
||||
homeserverUrl: context.homeserverUrl,
|
||||
identityServerUrl: context.identityServerUrl,
|
||||
accessToken: bearerToken.access_token,
|
||||
refreshToken: bearerToken.refresh_token,
|
||||
clientId: context.authContext.clientId,
|
||||
};
|
||||
};
|
||||
@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type ReactNode } from "react";
|
||||
import { OidcError } from "matrix-js-sdk/src/oidc/error";
|
||||
import { OAuth2Error } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { _t } from "../../languageHandler";
|
||||
|
||||
@@ -15,8 +15,7 @@ import { _t } from "../../languageHandler";
|
||||
* Errors thrown by EW during OIDC native flow authentication.
|
||||
* Intended to be logged, not read by users.
|
||||
*/
|
||||
export enum OidcClientError {
|
||||
InvalidQueryParameters = "Invalid query parameters for OIDC native login. `code` and `state` are required.",
|
||||
export enum OAuthClientError {
|
||||
InvalidFragmentParameters = "Invalid fragment parameters for OIDC native login. `code` and `state` are required.",
|
||||
}
|
||||
|
||||
@@ -26,15 +25,13 @@ export enum OidcClientError {
|
||||
* @param error
|
||||
* @returns a friendly translated error message for user consumption
|
||||
*/
|
||||
export const getOidcErrorMessage = (error: Error): string | ReactNode => {
|
||||
export const getOAuthErrorMessage = (error: Error): string | ReactNode => {
|
||||
switch (error.message) {
|
||||
case OidcError.MissingOrInvalidStoredState:
|
||||
case OAuth2Error.MissingOrInvalidStoredState:
|
||||
return _t("auth|oidc|missing_or_invalid_stored_state");
|
||||
case OidcClientError.InvalidQueryParameters:
|
||||
case OidcClientError.InvalidFragmentParameters:
|
||||
case OidcError.CodeExchangeFailed:
|
||||
case OidcError.InvalidBearerTokenResponse:
|
||||
case OidcError.InvalidIdToken:
|
||||
case OAuthClientError.InvalidFragmentParameters:
|
||||
case OAuth2Error.CodeExchangeFailed:
|
||||
case OAuth2Error.InvalidBearerTokenResponse:
|
||||
default:
|
||||
return _t("auth|oidc|generic_auth_error");
|
||||
}
|
||||
+2
-2
@@ -6,7 +6,7 @@ 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 { type OidcClientConfig } from "matrix-js-sdk/src/matrix";
|
||||
import { type ValidatedAuthMetadata } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
/**
|
||||
* Check the create prompt is supported by the OP, if so, we can do a registration flow
|
||||
@@ -14,7 +14,7 @@ import { type OidcClientConfig } from "matrix-js-sdk/src/matrix";
|
||||
* @param delegatedAuthConfig config as returned from discovery
|
||||
* @returns whether user registration is supported
|
||||
*/
|
||||
export const isUserRegistrationSupported = (delegatedAuthConfig: OidcClientConfig): boolean => {
|
||||
export const isUserRegistrationSupported = (delegatedAuthConfig: ValidatedAuthMetadata): boolean => {
|
||||
const supportedPrompts = delegatedAuthConfig.prompt_values_supported;
|
||||
return Array.isArray(supportedPrompts) && supportedPrompts?.includes("create");
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { vi, describe, it, expect, beforeEach } from "vitest";
|
||||
|
||||
import { getStoredOAuthClientId, persistOAuthClientId } from "./persistOAuthSettings";
|
||||
|
||||
describe("persist OAuth2 settings", () => {
|
||||
vi.spyOn(localStorage, "getItem");
|
||||
vi.spyOn(localStorage, "setItem");
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
const clientId = "test-client-id";
|
||||
|
||||
describe("persistOAuthClientId", () => {
|
||||
it("should set clientId in localStorage", () => {
|
||||
persistOAuthClientId(clientId);
|
||||
expect(localStorage.setItem).toHaveBeenCalledWith("mx_oidc_client_id", clientId);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getStoredOAuthClientId()", () => {
|
||||
it("should return clientId from localStorage", () => {
|
||||
localStorage.setItem("mx_oidc_client_id", clientId);
|
||||
expect(getStoredOAuthClientId()).toEqual(clientId);
|
||||
expect(localStorage.getItem).toHaveBeenCalledWith("mx_oidc_client_id");
|
||||
});
|
||||
it("should throw when no clientId in localStorage", () => {
|
||||
expect(() => getStoredOAuthClientId()).toThrow("OAuth client ID not found in storage");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
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 { type OAuth2, type ValidatedAuthMetadata } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import PlatformPeg from "../../PlatformPeg.ts";
|
||||
|
||||
const clientIdLocalStorageKey = "mx_oidc_client_id";
|
||||
const stateSessionStorageKey = "mx_oauth_state";
|
||||
|
||||
/**
|
||||
* Persists OAuth2 clientId in local storage
|
||||
* Only set after successful authentication
|
||||
* @param clientId
|
||||
*/
|
||||
export const persistOAuthClientId = (clientId: string): void => {
|
||||
localStorage.setItem(clientIdLocalStorageKey, clientId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves stored oauth client id from local storage.
|
||||
* The presence of a client ID in storage implies that the user is authenticated via OAuth.
|
||||
* @returns clientId
|
||||
* @throws when clientId is not found in local storage
|
||||
*/
|
||||
export const getStoredOAuthClientId = (): string => {
|
||||
const clientId = localStorage.getItem(clientIdLocalStorageKey);
|
||||
if (!clientId) {
|
||||
throw new Error("OAuth client ID not found in storage");
|
||||
}
|
||||
return clientId;
|
||||
};
|
||||
|
||||
type OAuth2Context = ConstructorParameters<typeof OAuth2>[1];
|
||||
|
||||
/**
|
||||
* Utility function to get the OAuth parameters needed to construct an OAuth2 instance
|
||||
* @param clientId - the registered OAuth client ID
|
||||
*/
|
||||
export function getOAuthParams(clientId: string): OAuth2Context {
|
||||
const platform = PlatformPeg.get()!;
|
||||
const redirectUri = platform.getOAuthCallbackUrl().href;
|
||||
return { clientId, redirectUri };
|
||||
}
|
||||
|
||||
/**
|
||||
* Temporary context for authorization code flow
|
||||
* Persisted via sessionStorage to be recalled when authentication navigates the tab away and back again
|
||||
*/
|
||||
export interface Context {
|
||||
/** The state string we included in the auth url */
|
||||
state: string;
|
||||
/** The URL of the homeserver the user is logging into */
|
||||
homeserverUrl: string;
|
||||
/** The URL of the identity server the user is using */
|
||||
identityServerUrl: string | undefined;
|
||||
/** The metadata received from {@link MatrixClient.getAuthMetadata} at time of initiating the auth dance */
|
||||
metadata: ValidatedAuthMetadata;
|
||||
/** The context needed for the SDK's OAuth2 to resume the auth flow */
|
||||
authContext: Required<OAuth2Context>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the context of the ongoing authorization code flow from sessionStorage
|
||||
*/
|
||||
export function loadAuthContext(): Context | null {
|
||||
const value = sessionStorage.getItem(stateSessionStorageKey);
|
||||
return JSON.parse(value!);
|
||||
}
|
||||
|
||||
/**
|
||||
* Temporary storage for the authorization code flow
|
||||
* @param context - the data to store in sessionStorage
|
||||
*/
|
||||
export function storeAuthContext(context: Context): void {
|
||||
sessionStorage.setItem(stateSessionStorageKey, JSON.stringify(context));
|
||||
}
|
||||
+13
-13
@@ -7,43 +7,43 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { registerOidcClient, type OidcClientConfig } from "matrix-js-sdk/src/matrix";
|
||||
import { OAuth2, type ValidatedAuthMetadata } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { type IConfigOptions } from "../../IConfigOptions";
|
||||
import PlatformPeg from "../../PlatformPeg";
|
||||
|
||||
/**
|
||||
* Get the statically configured clientId for the issuer
|
||||
* @param issuer delegated auth OIDC issuer
|
||||
* @param staticOidcClients static client config from config.json
|
||||
* @param issuer delegated auth OAuth2 issuer
|
||||
* @param staticOAuthClients static client config from config.json
|
||||
* @returns clientId if found, otherwise undefined
|
||||
*/
|
||||
const getStaticOidcClientId = (
|
||||
const getStaticOAuthClientId = (
|
||||
issuer: string,
|
||||
staticOidcClients?: IConfigOptions["oidc_static_clients"],
|
||||
staticOAuthClients?: IConfigOptions["oidc_static_clients"],
|
||||
): string | undefined => {
|
||||
// static_oidc_clients are configured with a trailing slash
|
||||
const issuerWithTrailingSlash = issuer.endsWith("/") ? issuer : issuer + "/";
|
||||
return staticOidcClients?.[issuerWithTrailingSlash]?.client_id;
|
||||
return staticOAuthClients?.[issuerWithTrailingSlash]?.client_id;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the clientId for an OIDC OP
|
||||
* Get the clientId for an OAuth2 OP
|
||||
* Checks statically configured clientIds first
|
||||
* Then attempts dynamic registration with the OP
|
||||
* @param delegatedAuthConfig Auth config from ValidatedServerConfig
|
||||
* @param staticOidcClients static client config from config.json
|
||||
* @param staticOAuthClients static client config from config.json
|
||||
* @returns Promise<string> resolves with clientId
|
||||
* @throws if no clientId is found
|
||||
*/
|
||||
export const getOidcClientId = async (
|
||||
delegatedAuthConfig: OidcClientConfig,
|
||||
staticOidcClients?: IConfigOptions["oidc_static_clients"],
|
||||
export const getOAuthClientId = async (
|
||||
delegatedAuthConfig: ValidatedAuthMetadata,
|
||||
staticOAuthClients?: IConfigOptions["oidc_static_clients"],
|
||||
): Promise<string> => {
|
||||
const staticClientId = getStaticOidcClientId(delegatedAuthConfig.issuer, staticOidcClients);
|
||||
const staticClientId = getStaticOAuthClientId(delegatedAuthConfig.issuer, staticOAuthClients);
|
||||
if (staticClientId) {
|
||||
logger.debug(`Using static clientId for issuer ${delegatedAuthConfig.issuer}`);
|
||||
return staticClientId;
|
||||
}
|
||||
return await registerOidcClient(delegatedAuthConfig, await PlatformPeg.get()!.getOidcClientMetadata());
|
||||
return await OAuth2.registerClient(delegatedAuthConfig, await PlatformPeg.get()!.getOAuthClientMetadata());
|
||||
};
|
||||
@@ -1,36 +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 { OidcTokenRefresher, type AccessTokens } from "matrix-js-sdk/src/matrix";
|
||||
import { type IdTokenClaims } from "oidc-client-ts";
|
||||
|
||||
import PlatformPeg from "../../PlatformPeg";
|
||||
import { persistAccessTokenInStorage, persistRefreshTokenInStorage } from "../tokens/tokens";
|
||||
|
||||
/**
|
||||
* OidcTokenRefresher that implements token persistence.
|
||||
* Stores tokens in the same way as login flow in Lifecycle.
|
||||
*/
|
||||
export class TokenRefresher extends OidcTokenRefresher {
|
||||
public constructor(
|
||||
issuer: string,
|
||||
clientId: string,
|
||||
redirectUri: string,
|
||||
deviceId: string,
|
||||
idTokenClaims: IdTokenClaims,
|
||||
private readonly userId: string,
|
||||
) {
|
||||
super(issuer, clientId, redirectUri, deviceId, idTokenClaims);
|
||||
}
|
||||
|
||||
public async persistTokens({ accessToken, refreshToken }: AccessTokens): Promise<void> {
|
||||
const pickleKey = (await PlatformPeg.get()?.getPickleKey(this.userId, this.deviceId)) ?? undefined;
|
||||
await persistAccessTokenInStorage(accessToken, pickleKey);
|
||||
await persistRefreshTokenInStorage(refreshToken, pickleKey);
|
||||
}
|
||||
}
|
||||
@@ -1,143 +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 { completeAuthorizationCodeGrant, generateOidcAuthorizationUrl } from "matrix-js-sdk/src/oidc/authorize";
|
||||
import { type OidcClientConfig } from "matrix-js-sdk/src/matrix";
|
||||
import { secureRandomString } from "matrix-js-sdk/src/randomstring";
|
||||
import { type IdTokenClaims } from "oidc-client-ts";
|
||||
|
||||
import { OidcClientError } from "./error";
|
||||
import PlatformPeg from "../../PlatformPeg";
|
||||
import { type URLParams } from "../../vector/url_utils.ts";
|
||||
|
||||
/**
|
||||
* Start OIDC authorization code flow
|
||||
* Generates auth params, stores them in session storage and
|
||||
* Navigates to configured authorization endpoint
|
||||
* @param delegatedAuthConfig from discovery
|
||||
* @param clientId this client's id as registered with configured issuer
|
||||
* @param homeserverUrl target homeserver
|
||||
* @param identityServerUrl OPTIONAL target identity server
|
||||
* @param isRegistration if true will set the prompt to "create"
|
||||
* @returns Promise that resolves after we have navigated to auth endpoint
|
||||
*/
|
||||
export const startOidcLogin = async (
|
||||
delegatedAuthConfig: OidcClientConfig,
|
||||
clientId: string,
|
||||
homeserverUrl: string,
|
||||
identityServerUrl?: string,
|
||||
isRegistration?: boolean,
|
||||
): Promise<void> => {
|
||||
const redirectUri = PlatformPeg.get()!.getOidcCallbackUrl().href;
|
||||
|
||||
const nonce = secureRandomString(10);
|
||||
|
||||
const prompt = isRegistration ? "create" : undefined;
|
||||
|
||||
const authorizationUrl = await generateOidcAuthorizationUrl({
|
||||
metadata: delegatedAuthConfig,
|
||||
redirectUri,
|
||||
clientId,
|
||||
homeserverUrl,
|
||||
identityServerUrl,
|
||||
nonce,
|
||||
prompt,
|
||||
urlState: PlatformPeg.get()?.getOidcClientState(),
|
||||
responseMode: delegatedAuthConfig.response_modes_supported?.includes("fragment") ? "fragment" : "query",
|
||||
});
|
||||
|
||||
window.location.href = authorizationUrl;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets `code` and `state` response params
|
||||
*
|
||||
* @param urlParams - the parameters to read
|
||||
* @param responseMode - the response_mode used in the auth request
|
||||
* @returns code and state
|
||||
* @throws when code and state are not valid strings
|
||||
*/
|
||||
const getCodeAndStateFromParams = (
|
||||
{ code, state }: NonNullable<URLParams["oidc_fragment"]>,
|
||||
responseMode: "fragment" | "query",
|
||||
): { code: string; state: string } => {
|
||||
if (!code || typeof code !== "string" || !state || typeof state !== "string") {
|
||||
if (responseMode === "fragment") {
|
||||
throw new Error(OidcClientError.InvalidFragmentParameters);
|
||||
} else {
|
||||
throw new Error(OidcClientError.InvalidQueryParameters);
|
||||
}
|
||||
}
|
||||
return { code, state };
|
||||
};
|
||||
|
||||
/**
|
||||
* Return type for {@link completeOidcLogin}
|
||||
* Contains all the credentials gathered from a successful OIDC login
|
||||
*/
|
||||
export type CompleteOidcLoginResponse = {
|
||||
/**
|
||||
* URL of the homeserver selected during login
|
||||
*/
|
||||
homeserverUrl: string;
|
||||
/**
|
||||
* Identity server URL as discovered during login
|
||||
*/
|
||||
identityServerUrl?: string;
|
||||
/**
|
||||
* Access Token gained from OIDC token issuer
|
||||
*/
|
||||
accessToken: string;
|
||||
/**
|
||||
* Refresh Token gained from OIDC token issuer, when falsy token cannot be refreshed
|
||||
*/
|
||||
refreshToken?: string;
|
||||
/**
|
||||
* ID Token gained from OIDC token issuer
|
||||
*/
|
||||
idToken?: string;
|
||||
/**
|
||||
* This client's ID as registered with the OIDC issuer
|
||||
*/
|
||||
clientId: string;
|
||||
/**
|
||||
* Issuer used during authentication
|
||||
*/
|
||||
issuer: string;
|
||||
/**
|
||||
* Claims of the given access token; used during token refresh to validate new tokens
|
||||
*/
|
||||
idTokenClaims: IdTokenClaims;
|
||||
};
|
||||
|
||||
/**
|
||||
* Attempt to complete authorization code flow to get an access token
|
||||
* @param urlParams the parameters extracted from the app-load URI.
|
||||
* @param responseMode - the response_mode used in the auth request
|
||||
* @returns Promise that resolves with a CompleteOidcLoginResponse when login was successful
|
||||
* @throws When we failed to get a valid access token
|
||||
*/
|
||||
export const completeOidcLogin = async (
|
||||
urlParams: NonNullable<URLParams["oidc_fragment"]>,
|
||||
responseMode: "fragment" | "query",
|
||||
): Promise<CompleteOidcLoginResponse> => {
|
||||
const { code, state } = getCodeAndStateFromParams(urlParams, responseMode);
|
||||
const { homeserverUrl, tokenResponse, idTokenClaims, identityServerUrl, oidcClientSettings } =
|
||||
await completeAuthorizationCodeGrant(code, state, responseMode);
|
||||
|
||||
return {
|
||||
homeserverUrl,
|
||||
identityServerUrl,
|
||||
accessToken: tokenResponse.access_token,
|
||||
refreshToken: tokenResponse.refresh_token,
|
||||
idToken: tokenResponse.id_token,
|
||||
clientId: oidcClientSettings.clientId,
|
||||
issuer: oidcClientSettings.issuer,
|
||||
idTokenClaims,
|
||||
};
|
||||
};
|
||||
@@ -1,116 +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.
|
||||
*/
|
||||
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { vi, describe, it, expect, beforeEach } from "vitest";
|
||||
import { type IdTokenClaims } from "oidc-client-ts";
|
||||
import { decodeIdToken } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import {
|
||||
getStoredOidcClientId,
|
||||
getStoredOidcIdToken,
|
||||
getStoredOidcIdTokenClaims,
|
||||
getStoredOidcTokenIssuer,
|
||||
persistOidcAuthenticatedSettings,
|
||||
} from "./persistOidcSettings";
|
||||
|
||||
vi.mock("matrix-js-sdk/src/matrix");
|
||||
|
||||
describe("persist OIDC settings", () => {
|
||||
vi.spyOn(localStorage, "getItem");
|
||||
vi.spyOn(localStorage, "setItem");
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
const clientId = "test-client-id";
|
||||
const issuer = "https://auth.org/";
|
||||
const idToken = "test-id-token";
|
||||
const idTokenClaims: IdTokenClaims = {
|
||||
// audience is this client
|
||||
aud: "123",
|
||||
// issuer matches
|
||||
iss: issuer,
|
||||
sub: "123",
|
||||
exp: 123,
|
||||
iat: 456,
|
||||
};
|
||||
|
||||
describe("persistOidcAuthenticatedSettings", () => {
|
||||
it("should set clientId and issuer in localStorage", () => {
|
||||
persistOidcAuthenticatedSettings(clientId, issuer, idToken);
|
||||
expect(localStorage.setItem).toHaveBeenCalledWith("mx_oidc_client_id", clientId);
|
||||
expect(localStorage.setItem).toHaveBeenCalledWith("mx_oidc_token_issuer", issuer);
|
||||
expect(localStorage.setItem).toHaveBeenCalledWith("mx_oidc_id_token", idToken);
|
||||
});
|
||||
|
||||
it("should not set idToken in localStorage when idToken is undefined", () => {
|
||||
persistOidcAuthenticatedSettings(clientId, issuer, undefined);
|
||||
expect(localStorage.setItem).toHaveBeenCalledWith("mx_oidc_client_id", clientId);
|
||||
expect(localStorage.setItem).toHaveBeenCalledWith("mx_oidc_token_issuer", issuer);
|
||||
expect(localStorage.getItem("mx_oidc_id_token")).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getStoredOidcTokenIssuer()", () => {
|
||||
it("should return issuer from localStorage", () => {
|
||||
localStorage.setItem("mx_oidc_token_issuer", issuer);
|
||||
expect(getStoredOidcTokenIssuer()).toEqual(issuer);
|
||||
expect(localStorage.getItem).toHaveBeenCalledWith("mx_oidc_token_issuer");
|
||||
});
|
||||
|
||||
it("should return undefined when no issuer in localStorage", () => {
|
||||
expect(getStoredOidcTokenIssuer()).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getStoredOidcClientId()", () => {
|
||||
it("should return clientId from localStorage", () => {
|
||||
localStorage.setItem("mx_oidc_client_id", clientId);
|
||||
expect(getStoredOidcClientId()).toEqual(clientId);
|
||||
expect(localStorage.getItem).toHaveBeenCalledWith("mx_oidc_client_id");
|
||||
});
|
||||
it("should throw when no clientId in localStorage", () => {
|
||||
expect(() => getStoredOidcClientId()).toThrow("Oidc client id not found in storage");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getStoredOidcIdToken()", () => {
|
||||
it("should return token from localStorage", () => {
|
||||
localStorage.setItem("mx_oidc_id_token", idToken);
|
||||
expect(getStoredOidcIdToken()).toEqual(idToken);
|
||||
expect(localStorage.getItem).toHaveBeenCalledWith("mx_oidc_id_token");
|
||||
});
|
||||
|
||||
it("should return undefined when no token in localStorage", () => {
|
||||
expect(getStoredOidcIdToken()).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getStoredOidcIdTokenClaims()", () => {
|
||||
it("should return claims from localStorage", () => {
|
||||
localStorage.setItem("mx_oidc_id_token_claims", JSON.stringify(idTokenClaims));
|
||||
expect(getStoredOidcIdTokenClaims()).toEqual(idTokenClaims);
|
||||
expect(localStorage.getItem).toHaveBeenCalledWith("mx_oidc_id_token_claims");
|
||||
});
|
||||
|
||||
it("should return claims extracted from id_token in localStorage", () => {
|
||||
localStorage.setItem("mx_oidc_id_token", idToken);
|
||||
vi.mocked(decodeIdToken).mockReturnValue(idTokenClaims);
|
||||
expect(getStoredOidcIdTokenClaims()).toEqual(idTokenClaims);
|
||||
expect(decodeIdToken).toHaveBeenCalledWith(idToken);
|
||||
expect(localStorage.getItem).toHaveBeenCalledWith("mx_oidc_id_token_claims");
|
||||
});
|
||||
|
||||
it("should return undefined when no claims in localStorage", () => {
|
||||
expect(getStoredOidcIdTokenClaims()).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,85 +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 { type IdTokenClaims } from "oidc-client-ts";
|
||||
import { decodeIdToken } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
const clientIdStorageKey = "mx_oidc_client_id";
|
||||
const tokenIssuerStorageKey = "mx_oidc_token_issuer";
|
||||
const idTokenStorageKey = "mx_oidc_id_token";
|
||||
/**
|
||||
* @deprecated in favour of using idTokenStorageKey
|
||||
*/
|
||||
const idTokenClaimsStorageKey = "mx_oidc_id_token_claims";
|
||||
|
||||
/**
|
||||
* Persists oidc clientId and issuer in local storage
|
||||
* Only set after successful authentication
|
||||
* @param clientId
|
||||
* @param issuer
|
||||
* @param idToken
|
||||
* @param idTokenClaims
|
||||
*/
|
||||
export const persistOidcAuthenticatedSettings = (
|
||||
clientId: string,
|
||||
issuer: string,
|
||||
idToken: string | undefined,
|
||||
): void => {
|
||||
localStorage.setItem(clientIdStorageKey, clientId);
|
||||
localStorage.setItem(tokenIssuerStorageKey, issuer);
|
||||
if (idToken) {
|
||||
localStorage.setItem(idTokenStorageKey, idToken);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve stored oidc issuer from local storage
|
||||
* When user has token from OIDC issuer, this will be set
|
||||
* @returns issuer or undefined
|
||||
*/
|
||||
export const getStoredOidcTokenIssuer = (): string | undefined => {
|
||||
return localStorage.getItem(tokenIssuerStorageKey) ?? undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves stored oidc client id from local storage
|
||||
* @returns clientId
|
||||
* @throws when clientId is not found in local storage
|
||||
*/
|
||||
export const getStoredOidcClientId = (): string => {
|
||||
const clientId = localStorage.getItem(clientIdStorageKey);
|
||||
if (!clientId) {
|
||||
throw new Error("Oidc client id not found in storage");
|
||||
}
|
||||
return clientId;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve stored id token claims from stored id token or local storage
|
||||
* @returns idTokenClaims or undefined
|
||||
*/
|
||||
export const getStoredOidcIdTokenClaims = (): IdTokenClaims | undefined => {
|
||||
const idToken = getStoredOidcIdToken();
|
||||
if (idToken) {
|
||||
return decodeIdToken(idToken);
|
||||
}
|
||||
|
||||
const idTokenClaims = localStorage.getItem(idTokenClaimsStorageKey);
|
||||
if (!idTokenClaims) {
|
||||
return;
|
||||
}
|
||||
return JSON.parse(idTokenClaims) as IdTokenClaims;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve stored id token from local storage
|
||||
* @returns idToken or undefined
|
||||
*/
|
||||
export const getStoredOidcIdToken = (): string | undefined => {
|
||||
return localStorage.getItem(idTokenStorageKey) ?? undefined;
|
||||
};
|
||||
@@ -7,6 +7,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { type AccessTokens } from "matrix-js-sdk/src/matrix";
|
||||
import decryptAESSecretStorageItem from "matrix-js-sdk/src/utils/decryptAESSecretStorageItem";
|
||||
import encryptAESSecretStorageItem from "matrix-js-sdk/src/utils/encryptAESSecretStorageItem";
|
||||
import { type AESEncryptedSecretStoragePayload } from "matrix-js-sdk/src/types";
|
||||
@@ -116,7 +117,7 @@ export async function tryDecryptToken(
|
||||
* @param hasTokenStorageKey Localstorage key for an item which stores whether we expect to have a token in indexeddb,
|
||||
* eg "mx_has_access_token".
|
||||
*/
|
||||
export async function persistTokenInStorage(
|
||||
async function persistTokenInStorage(
|
||||
storageKey: string,
|
||||
tokenName: string,
|
||||
token: string | undefined,
|
||||
@@ -174,40 +175,24 @@ export async function persistTokenInStorage(
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps {@link persistTokenInStorage} with accessToken storage keys
|
||||
* Wraps {@link persistTokenInStorage} with accessToken & refreshToken storage keys
|
||||
*
|
||||
* @param token - The token to store. When undefined, any existing accessToken is removed from storage.
|
||||
* @param pickleKey - Pickle key: used to derive the key used to encrypt token. If `undefined`, the token will be stored
|
||||
* unencrypted.
|
||||
* @param tokens - The tokens to persist
|
||||
* @param pickleKey - Pickle key: used to derive the key used to encrypt token.
|
||||
* If `undefined`, the token will be stored unencrypted.
|
||||
*/
|
||||
export async function persistAccessTokenInStorage(
|
||||
token: string | undefined,
|
||||
pickleKey: string | undefined,
|
||||
): Promise<void> {
|
||||
return persistTokenInStorage(
|
||||
export async function persistTokens(pickleKey: string | undefined, tokens: AccessTokens): Promise<void> {
|
||||
await persistTokenInStorage(
|
||||
ACCESS_TOKEN_STORAGE_KEY,
|
||||
ACCESS_TOKEN_IV,
|
||||
token,
|
||||
tokens.accessToken,
|
||||
pickleKey,
|
||||
HAS_ACCESS_TOKEN_STORAGE_KEY,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps {@link persistTokenInStorage} with refreshToken storage keys.
|
||||
*
|
||||
* @param token - The token to store. When undefined, any existing refreshToken is removed from storage.
|
||||
* @param pickleKey - Pickle key: used to derive the key used to encrypt token. If `undefined`, the token will be stored
|
||||
* unencrypted.
|
||||
*/
|
||||
export async function persistRefreshTokenInStorage(
|
||||
token: string | undefined,
|
||||
pickleKey: string | undefined,
|
||||
): Promise<void> {
|
||||
return persistTokenInStorage(
|
||||
await persistTokenInStorage(
|
||||
REFRESH_TOKEN_STORAGE_KEY,
|
||||
REFRESH_TOKEN_IV,
|
||||
token,
|
||||
tokens.refreshToken,
|
||||
pickleKey,
|
||||
HAS_REFRESH_TOKEN_STORAGE_KEY,
|
||||
);
|
||||
|
||||
@@ -12,7 +12,7 @@ import { vi, describe, it, expect, afterAll, beforeEach } from "vitest";
|
||||
import fetchMock from "@fetch-mock/vitest";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { mockPlatformPeg, unmockPlatformPeg } from "test-utils";
|
||||
import { makeDelegatedAuthConfig } from "test-utils/oidc";
|
||||
import { makeDelegatedAuthMetadata } from "test-utils/auth";
|
||||
import { type RefCallback } from "react";
|
||||
|
||||
import { loadApp } from "./app.tsx";
|
||||
@@ -54,7 +54,7 @@ describe("sso_redirect_options", () => {
|
||||
oidc_static_clients: { [issuer]: { client_id: "12345" } },
|
||||
});
|
||||
// Signal we support v1.1 to pass the minimum js-sdk compatibility bar
|
||||
// Signal we support v1.15 to use stable Native OIDC support
|
||||
// Signal we support v1.15 to use stable Native OAuth2 support
|
||||
fetchMock.get("https://synapse/_matrix/client/versions", { versions: ["v1.1", "v1.15"] });
|
||||
});
|
||||
|
||||
@@ -69,17 +69,18 @@ describe("sso_redirect_options", () => {
|
||||
expect(startSingleSignOnSpy).toHaveBeenCalledWith(expect.any(MatrixClient), "sso", "/room/#room:server");
|
||||
});
|
||||
|
||||
it("should redirect for native OIDC", async () => {
|
||||
const authConfig = { ...makeDelegatedAuthConfig(issuer), response_modes_supported: ["query", "fragment"] };
|
||||
it("should redirect for native OAuth2", async () => {
|
||||
const authConfig = {
|
||||
...makeDelegatedAuthMetadata(issuer),
|
||||
response_modes_supported: ["query", "fragment"],
|
||||
};
|
||||
fetchMock.get("https://synapse/_matrix/client/v1/auth_metadata", authConfig);
|
||||
fetchMock.get(`${authConfig.issuer}.well-known/openid-configuration`, authConfig);
|
||||
fetchMock.get(authConfig.jwks_uri!, { keys: [] });
|
||||
|
||||
const startOidcLoginSpy = vi.spyOn(window.location, "href", "set");
|
||||
const startOAuthLoginSpy = vi.spyOn(window.location, "href", "set");
|
||||
|
||||
await loadApp({}, vi.fn() as RefCallback<MatrixChat>);
|
||||
expect(startOidcLoginSpy).toHaveBeenCalledWith(
|
||||
"https://auth.org/auth?client_id=12345&redirect_uri=https%3A%2F%2Fapp.element.io%2F%3Fno_universal_links%3Dtrue&response_type=code&scope=openid+urn%3Amatrix%3Aorg.matrix.msc2967.client%3Aapi%3A*+urn%3Amatrix%3Aorg.matrix.msc2967.client%3Adevice%3AABCDEFGHIJ&nonce=ABCDEFGHIJ&state=10000000100040008000100000000000&code_challenge=awE81eIsGff70JahvrTqWRbGKLI10ooyo_Xm1sxuZvU&code_challenge_method=S256&response_mode=fragment",
|
||||
expect(startOAuthLoginSpy).toHaveBeenCalledWith(
|
||||
"https://auth.org/auth?response_type=code&response_mode=fragment&client_id=12345&redirect_uri=https%3A%2F%2Fapp.element.io%2F%3Fno_universal_links%3Dtrue&scope=urn%3Amatrix%3Aclient%3Aapi%3A*+urn%3Amatrix%3Aclient%3Adevice%3AABCDEFGHIJ&state=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef&code_challenge_method=S256&code_challenge=ymW9_yTzfYF1Km4N7W4OC6jQ7xoj91DUulQHWfmrROM",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -34,7 +34,7 @@ import { RoomView } from "../components/structures/RoomView";
|
||||
import RoomAvatar from "../components/views/avatars/RoomAvatar";
|
||||
import { ModuleNotificationDecoration } from "../modules/components/ModuleNotificationDecoration";
|
||||
import Login from "../Login.ts";
|
||||
import { startOidcLogin } from "../utils/oidc/authorize.ts";
|
||||
import { startOAuthLogin } from "../utils/oauth/authorize.ts";
|
||||
|
||||
logger.log(`Application is running in ${process.env.NODE_ENV} mode`);
|
||||
|
||||
@@ -44,7 +44,7 @@ function onTokenLoginCompleted(urlParams: URLParams, fragmentAfterLogin: string)
|
||||
const url = new URL(window.location.href);
|
||||
|
||||
// if we did a token login, we're now left with the login token as query param in the url; clear it out
|
||||
for (const param in { ...urlParams.legacy_sso, ...urlParams.oidc_query }) {
|
||||
for (const param in { ...urlParams.legacy_sso }) {
|
||||
url.searchParams.delete(param);
|
||||
}
|
||||
|
||||
@@ -68,9 +68,9 @@ async function redirectToSso(config: ValidatedServerConfig): Promise<boolean> {
|
||||
});
|
||||
const flows = await login.getFlows();
|
||||
|
||||
const nativeOidcFlow = flows.find((flow) => "clientId" in flow);
|
||||
if (nativeOidcFlow && config.delegatedAuthentication) {
|
||||
await startOidcLogin(config.delegatedAuthentication, nativeOidcFlow.clientId, config.hsUrl, config.isUrl);
|
||||
const nativeOAuthFlow = flows.find((flow) => "clientId" in flow);
|
||||
if (nativeOAuthFlow && config.delegatedAuthentication) {
|
||||
await startOAuthLogin(config.delegatedAuthentication, nativeOAuthFlow.clientId, config.hsUrl, config.isUrl);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ export async function loadApp(urlParams: URLParams, matrixChatRef: React.Ref<Mat
|
||||
// Before we continue, let's see if we're supposed to do an SSO redirect
|
||||
const [userId] = await Lifecycle.getStoredSessionOwner();
|
||||
const hasPossibleToken = !!userId;
|
||||
const isReturningFromSso = !!urlParams.legacy_sso || !!urlParams.oidc_fragment || !!urlParams.oidc_query;
|
||||
const isReturningFromSso = !!urlParams.legacy_sso || !!urlParams.oauth2;
|
||||
const ssoRedirects = config.sso_redirect_options || {};
|
||||
let autoRedirect = ssoRedirects.immediate === true;
|
||||
// XXX: This path matching is a bit brittle, but better to do it early instead of in the app code.
|
||||
|
||||
@@ -86,7 +86,7 @@ describe("ElectronPlatform", () => {
|
||||
it("should return oidc client state as expected", async () => {
|
||||
const platform = new ElectronPlatform();
|
||||
await platform.getConfig();
|
||||
expect(platform.getOidcClientState()).toMatchInlineSnapshot(`":element-desktop-ssoid:session-id"`);
|
||||
expect(platform.getOAuthClientState()).toMatchInlineSnapshot(`":element-desktop-ssoid:session-id"`);
|
||||
});
|
||||
|
||||
it("dispatches view settings action on preferences event", () => {
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
type MatrixClient,
|
||||
type Room,
|
||||
type MatrixEvent,
|
||||
type OidcRegistrationClientMetadata,
|
||||
type OAuthRegistrationRequest,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import React from "react";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
@@ -531,28 +531,28 @@ export default class ElectronPlatform extends BasePlatform {
|
||||
return (SdkConfig.get() as unknown as Record<string, string>)["web_base_url"] ?? "https://app.element.io";
|
||||
}
|
||||
|
||||
public get defaultOidcClientUri(): string {
|
||||
public get defaultOAuthClientUri(): string {
|
||||
// Default to element.io as our scheme `io.element.desktop` is within its scope on default MAS policies
|
||||
return "https://element.io";
|
||||
}
|
||||
|
||||
public async getOidcClientMetadata(): Promise<OidcRegistrationClientMetadata> {
|
||||
const baseMetadata = await super.getOidcClientMetadata();
|
||||
public async getOAuthClientMetadata(): Promise<OAuthRegistrationRequest> {
|
||||
const baseMetadata = await super.getOAuthClientMetadata();
|
||||
return {
|
||||
...baseMetadata,
|
||||
applicationType: "native",
|
||||
application_type: "native",
|
||||
};
|
||||
}
|
||||
|
||||
public getOidcClientState(): string {
|
||||
public getOAuthClientState(): string {
|
||||
return `:${SSO_ID_KEY}:${this.sessionId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* The URL to return to after a successful OIDC authentication
|
||||
*/
|
||||
public getOidcCallbackUrl(): URL {
|
||||
const url = super.getOidcCallbackUrl();
|
||||
public getOAuthCallbackUrl(): URL {
|
||||
const url = super.getOAuthCallbackUrl();
|
||||
url.protocol = this.protocol;
|
||||
// Trim the double slash into a single slash to comply with https://datatracker.ietf.org/doc/html/rfc8252#section-7.1
|
||||
if (url.href.startsWith(`${url.protocol}//`)) {
|
||||
|
||||
@@ -269,7 +269,7 @@ describe("WebPlatform", () => {
|
||||
expect(spy).toHaveBeenCalledWith(expect.anything(), { bgColor: "#f00" });
|
||||
});
|
||||
|
||||
describe("getOidcCallbackUrl()", () => {
|
||||
describe("getOAuthCallbackUrl()", () => {
|
||||
it("should not include the 'updated' query param in the redirect URI", () => {
|
||||
Object.defineProperty(window, "location", {
|
||||
value: {
|
||||
@@ -280,7 +280,7 @@ describe("WebPlatform", () => {
|
||||
writable: true,
|
||||
});
|
||||
const platform = new WebPlatform();
|
||||
const url = platform.getOidcCallbackUrl();
|
||||
const url = platform.getOAuthCallbackUrl();
|
||||
|
||||
expect(url.searchParams.has("updated")).toBe(false);
|
||||
expect(url.searchParams.get("no_universal_links")).toEqual("true");
|
||||
|
||||
@@ -45,18 +45,11 @@ describe("parseUrlParameters", () => {
|
||||
expect(parsed.params.legacy_sso?.loginToken).toEqual("foobar");
|
||||
});
|
||||
|
||||
it("should parse oidc parameters from fragment", () => {
|
||||
it("should parse oauth2 parameters from fragment", () => {
|
||||
const u = new URL("https://app.element.io/#code=foobar&state=barfoo");
|
||||
const parsed = parseAppUrl(u);
|
||||
expect(parsed.params.oidc_fragment?.code).toEqual("foobar");
|
||||
expect(parsed.params.oidc_fragment?.state).toEqual("barfoo");
|
||||
});
|
||||
|
||||
it("should parse oidc parameters from query", () => {
|
||||
const u = new URL("https://app.element.io/?code=foobar&state=barfoo");
|
||||
const parsed = parseAppUrl(u);
|
||||
expect(parsed.params.oidc_query?.code).toEqual("foobar");
|
||||
expect(parsed.params.oidc_query?.state).toEqual("barfoo");
|
||||
expect(parsed.params.oauth2?.code).toEqual("foobar");
|
||||
expect(parsed.params.oauth2?.state).toEqual("barfoo");
|
||||
});
|
||||
|
||||
it("should parse guest parameters", () => {
|
||||
|
||||
@@ -54,16 +54,11 @@ const urlParameterConfig = {
|
||||
keys: ["loginToken"],
|
||||
location: "query",
|
||||
},
|
||||
// Fragment params for OIDC login, added by the Identity Provider
|
||||
oidc_fragment: {
|
||||
// Fragment params for OAuth2 login, added by the Identity Provider
|
||||
oauth2: {
|
||||
keys: ["code", "state"],
|
||||
location: "fragment",
|
||||
},
|
||||
// Query params for OIDC login, added by the Identity Provider, used as fallback when fragment is unsupported
|
||||
oidc_query: {
|
||||
keys: ["code", "state"],
|
||||
location: "query",
|
||||
},
|
||||
// Fragment params relating to 3pid (email) invites, added in url within the invite email itself
|
||||
threepid: {
|
||||
keys: ["client_secret", "session_id", "hs_url", "is_url", "sid"],
|
||||
|
||||
@@ -39,7 +39,6 @@ export class UserMenuViewModel
|
||||
client: MatrixClient,
|
||||
ownProfileStore: OwnProfileStore,
|
||||
isPanelCollapsed: boolean,
|
||||
accountManagementEndpoint?: string,
|
||||
): UserMenuSnapshot {
|
||||
const hasHomePage = !!getHomePageUrl(SdkConfig.get(), client);
|
||||
const isAuthenticated = !client.isGuest();
|
||||
@@ -58,7 +57,7 @@ export class UserMenuViewModel
|
||||
displayName,
|
||||
avatarUrl,
|
||||
expanded: !isPanelCollapsed,
|
||||
manageAccountHref: accountManagementEndpoint,
|
||||
manageAccountHref: undefined, // loaded async
|
||||
showAvatar: isAuthenticated,
|
||||
userStatus: ownProfileStore.userStatus,
|
||||
showUserStatus: SettingsStore.getValue("feature_user_status") && isAuthenticated,
|
||||
@@ -80,19 +79,11 @@ export class UserMenuViewModel
|
||||
private readonly dispatcher: MatrixDispatcher,
|
||||
private readonly client: MatrixClient,
|
||||
isPanelCollapsed: boolean,
|
||||
accountManagementEndpoint?: string,
|
||||
) {
|
||||
super(
|
||||
props,
|
||||
UserMenuViewModel.computeSnapshot(
|
||||
client,
|
||||
props.ownProfileStore,
|
||||
isPanelCollapsed,
|
||||
accountManagementEndpoint,
|
||||
),
|
||||
);
|
||||
super(props, UserMenuViewModel.computeSnapshot(client, props.ownProfileStore, isPanelCollapsed));
|
||||
this.setStatusVm = new UserMenuSetStatusViewModel({ client, ownProfileStore: props.ownProfileStore });
|
||||
props.ownProfileStore.on(UPDATE_EVENT, this.recalculateProfile);
|
||||
this.loadAuthMetadata();
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
@@ -166,4 +157,9 @@ export class UserMenuViewModel
|
||||
logger.warn("Failed to clear user status", err);
|
||||
});
|
||||
};
|
||||
|
||||
private async loadAuthMetadata(): Promise<void> {
|
||||
const authMetadata = await this.client.getAuthMetadata().catch(() => {});
|
||||
this.snapshot.merge({ manageAccountHref: authMetadata?.account_management_uri });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
@@ -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",
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
+8
-17
@@ -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,
|
||||
|
||||
+6
-15
@@ -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,
|
||||
|
||||
+34
-53
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
+12
-38
@@ -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", () => {
|
||||
|
||||
@@ -252,7 +252,6 @@ export default (env: string, argv: Record<string, any>): webpack.Configuration =
|
||||
"@matrix-org/react-sdk-module-api": getPackageRoot("@matrix-org/react-sdk-module-api"),
|
||||
// and matrix-widget-api
|
||||
"matrix-widget-api": getPackageRoot("matrix-widget-api"),
|
||||
"oidc-client-ts": getPackageRoot("oidc-client-ts"),
|
||||
|
||||
// Make shared-components imports resolve to EW deps
|
||||
"@vector-im/compound-web": getPackageRoot("@vector-im/compound-web", ""),
|
||||
|
||||
@@ -137,7 +137,7 @@ export default withMermaid({
|
||||
{ text: "Memory profiling", link: "/memory-profiles-and-leaks.md" },
|
||||
{ text: "Jitsi", link: "/jitsi-dev.md" },
|
||||
{ text: "Feature flags", link: "/feature-flags.md" },
|
||||
{ text: "OIDC and delegated authentication", link: "/oidc.md" },
|
||||
{ text: "OAuth and delegated authentication", link: "/oauth.md" },
|
||||
{ text: "Release Process", link: "/release.md" },
|
||||
{ text: "MVVM", link: "/MVVM.md" },
|
||||
{ text: "Settings", link: "/settings.md" },
|
||||
|
||||
+1
-3
@@ -298,7 +298,6 @@ The following subproperties are available:
|
||||
2. `logo_uri`: Optional URI for the client logo.
|
||||
3. `tos_uri`: Optional URI for the client's terms of service.
|
||||
4. `policy_uri`: Optional URI for the client's privacy policy.
|
||||
5. `contacts`: Optional list of contact emails for the client.
|
||||
|
||||
As an example:
|
||||
|
||||
@@ -308,8 +307,7 @@ As an example:
|
||||
"client_uri": "https://example.com",
|
||||
"logo_uri": "https://example.com/logo.png",
|
||||
"tos_uri": "https://example.com/tos",
|
||||
"policy_uri": "https://example.com/policy",
|
||||
"contacts": ["support@example.com"]
|
||||
"policy_uri": "https://example.com/policy"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# OIDC and delegated authentication
|
||||
# OAuth2 and delegated authentication
|
||||
|
||||
See https://areweoidcyet.com/client-implementation-guide/ for implementation details.
|
||||
|
||||
Element Web uses [MSC2965: OIDC provider discovery](https://github.com/matrix-org/matrix-spec-proposals/pull/2965) to discover the configured provider.
|
||||
Where a valid MSC2965 configuration is discovered, OIDC native login flow will be the only login option offered.
|
||||
Element Web will attempt to [dynamically register](https://openid.net/specs/openid-connect-registration-1_0.html) with the configured OP.
|
||||
Element Web uses [/auth_metadata](https://spec.matrix.org/v1.18/client-server-api/#get_matrixclientv1auth_metadata) to discover the configured provider.
|
||||
Where a valid configuration is discovered, OAuth2 native login flow will be the only login option offered.
|
||||
Element Web will attempt to [dynamically register](https://spec.matrix.org/v1.18/client-server-api/#client-registration) with the configured OP.
|
||||
Then, authentication will be completed [as described here](https://areweoidcyet.com/client-implementation-guide/).
|
||||
|
||||
#### Statically configured OIDC clients
|
||||
#### Statically configured OAuth2 clients
|
||||
|
||||
Clients that are already registered with the OP can configure their `client_id` in `config.json`.
|
||||
Where static configuration exists for the OP dynamic client registration will not be attempted.
|
||||
Generated
+1
-6
@@ -261,8 +261,6 @@ overrides:
|
||||
pretty-format@30>react-is: 19.2.6
|
||||
'@types/react': ^19.2.10
|
||||
'@types/react-dom': ^19.2.3
|
||||
oidc-client-ts: 3.5.0
|
||||
jwt-decode: 4.0.0
|
||||
caniuse-lite: 1.0.30001799
|
||||
markdown-it: 14.1.1
|
||||
matrix-widget-api: ^1.17.0
|
||||
@@ -649,9 +647,6 @@ importers:
|
||||
mime:
|
||||
specifier: ^4.0.4
|
||||
version: 4.1.0
|
||||
oidc-client-ts:
|
||||
specifier: 3.5.0
|
||||
version: 3.5.0
|
||||
opus-recorder:
|
||||
specifier: ^8.0.3
|
||||
version: 8.0.5
|
||||
@@ -6563,7 +6558,7 @@ packages:
|
||||
focus-trap: ^7
|
||||
fuse.js: ^7
|
||||
idb-keyval: ^6
|
||||
jwt-decode: 4.0.0
|
||||
jwt-decode: ^4
|
||||
nprogress: ^0.2
|
||||
qrcode: ^1.5
|
||||
sortablejs: ^1
|
||||
|
||||
@@ -114,8 +114,6 @@ overrides:
|
||||
pretty-format@30>react-is: 19.2.6
|
||||
"@types/react": "catalog:"
|
||||
"@types/react-dom": "catalog:"
|
||||
oidc-client-ts: 3.5.0
|
||||
jwt-decode: 4.0.0
|
||||
caniuse-lite: 1.0.30001799
|
||||
markdown-it: 14.1.1
|
||||
matrix-widget-api: "^1.17.0"
|
||||
|
||||
Reference in New Issue
Block a user