Support build-time specified protocol scheme for oidc callback (#29814)

* Support build-time specified protocol scheme for oidc callback

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Add tests

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski
2025-05-22 11:24:53 +01:00
committed by GitHub
parent bd142412e5
commit f5125ac2b8
3 changed files with 40 additions and 10 deletions

View File

@@ -130,6 +130,11 @@ declare global {
interface Electron { interface Electron {
on(channel: ElectronChannel, listener: (event: Event, ...args: any[]) => void): void; on(channel: ElectronChannel, listener: (event: Event, ...args: any[]) => void): void;
send(channel: ElectronChannel, ...args: any[]): void; send(channel: ElectronChannel, ...args: any[]): void;
initialise(): Promise<{
protocol: string;
sessionId: string;
config: IConfigOptions;
}>;
} }
interface DesktopCapturerSource { interface DesktopCapturerSource {

View File

@@ -17,7 +17,6 @@ import {
type OidcRegistrationClientMetadata, type OidcRegistrationClientMetadata,
} from "matrix-js-sdk/src/matrix"; } from "matrix-js-sdk/src/matrix";
import React from "react"; import React from "react";
import { secureRandomString } from "matrix-js-sdk/src/randomstring";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import BasePlatform, { UpdateCheckStatus, type UpdateStatus } from "../../BasePlatform"; import BasePlatform, { UpdateCheckStatus, type UpdateStatus } from "../../BasePlatform";
@@ -97,8 +96,10 @@ function getUpdateCheckStatus(status: boolean | string): UpdateStatus {
export default class ElectronPlatform extends BasePlatform { export default class ElectronPlatform extends BasePlatform {
private readonly ipc = new IPCManager("ipcCall", "ipcReply"); private readonly ipc = new IPCManager("ipcCall", "ipcReply");
private readonly eventIndexManager: BaseEventIndexManager = new SeshatIndexManager(); private readonly eventIndexManager: BaseEventIndexManager = new SeshatIndexManager();
// this is the opaque token we pass to the HS which when we get it in our callback we can resolve to a profile private readonly initialised: Promise<void>;
private readonly ssoID: string = secureRandomString(32); private protocol!: string;
private sessionId!: string;
private config!: IConfigOptions;
public constructor() { public constructor() {
super(); super();
@@ -186,13 +187,21 @@ export default class ElectronPlatform extends BasePlatform {
await this.ipc.call("callDisplayMediaCallback", source ?? { id: "", name: "", thumbnailURL: "" }); await this.ipc.call("callDisplayMediaCallback", source ?? { id: "", name: "", thumbnailURL: "" });
}); });
void this.ipc.call("startSSOFlow", this.ssoID);
BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate); BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate);
this.initialised = this.initialise();
}
private async initialise(): Promise<void> {
const { protocol, sessionId, config } = await window.electron!.initialise();
this.protocol = protocol;
this.sessionId = sessionId;
this.config = config;
} }
public async getConfig(): Promise<IConfigOptions | undefined> { public async getConfig(): Promise<IConfigOptions | undefined> {
return this.ipc.call("getConfig"); await this.initialised;
return this.config;
} }
private onBreadcrumbsUpdate = (): void => { private onBreadcrumbsUpdate = (): void => {
@@ -391,7 +400,7 @@ export default class ElectronPlatform extends BasePlatform {
public getSSOCallbackUrl(fragmentAfterLogin?: string): URL { public getSSOCallbackUrl(fragmentAfterLogin?: string): URL {
const url = super.getSSOCallbackUrl(fragmentAfterLogin); const url = super.getSSOCallbackUrl(fragmentAfterLogin);
url.protocol = "element"; url.protocol = "element";
url.searchParams.set(SSO_ID_KEY, this.ssoID); url.searchParams.set(SSO_ID_KEY, this.sessionId);
return url; return url;
} }
@@ -469,7 +478,7 @@ export default class ElectronPlatform extends BasePlatform {
} }
public getOidcClientState(): string { public getOidcClientState(): string {
return `:${SSO_ID_KEY}:${this.ssoID}`; return `:${SSO_ID_KEY}:${this.sessionId}`;
} }
/** /**
@@ -477,7 +486,7 @@ export default class ElectronPlatform extends BasePlatform {
*/ */
public getOidcCallbackUrl(): URL { public getOidcCallbackUrl(): URL {
const url = super.getOidcCallbackUrl(); const url = super.getOidcCallbackUrl();
url.protocol = "io.element.desktop"; 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 // 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}//`)) { if (url.href.startsWith(`${url.protocol}//`)) {
url.href = url.href.replace("://", ":/"); url.href = url.href.replace("://", ":/");

View File

@@ -31,6 +31,11 @@ describe("ElectronPlatform", () => {
const mockElectron = { const mockElectron = {
on: jest.fn(), on: jest.fn(),
send: jest.fn(), send: jest.fn(),
initialise: jest.fn().mockResolvedValue({
protocol: "io.element.desktop",
sessionId: "session-id",
config: { _config: true },
}),
}; };
const dispatchSpy = jest.spyOn(dispatcher, "dispatch"); const dispatchSpy = jest.spyOn(dispatcher, "dispatch");
@@ -64,6 +69,17 @@ describe("ElectronPlatform", () => {
expect(rageshake.flush).toHaveBeenCalled(); expect(rageshake.flush).toHaveBeenCalled();
}); });
it("should load config", async () => {
const platform = new ElectronPlatform();
await expect(platform.getConfig()).resolves.toEqual({ _config: true });
});
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"`);
});
it("dispatches view settings action on preferences event", () => { it("dispatches view settings action on preferences event", () => {
new ElectronPlatform(); new ElectronPlatform();
const [event, handler] = getElectronEventHandlerCall("preferences")!; const [event, handler] = getElectronEventHandlerCall("preferences")!;
@@ -287,7 +303,7 @@ describe("ElectronPlatform", () => {
}); });
}); });
describe("breacrumbs", () => { describe("breadcrumbs", () => {
it("should send breadcrumb updates over the IPC", () => { it("should send breadcrumb updates over the IPC", () => {
const spy = jest.spyOn(BreadcrumbsStore.instance, "on"); const spy = jest.spyOn(BreadcrumbsStore.instance, "on");
new ElectronPlatform(); new ElectronPlatform();