Skip to content

Commit 84ca2b2

Browse files
committed
agent/runtime: fix browser auth bridge and macOS agent runtime
1 parent 7636e57 commit 84ca2b2

14 files changed

Lines changed: 512 additions & 82 deletions

File tree

browser-ext/src/background/background.ts

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,48 @@ import { sentry } from "../utils/sentry";
33

44
sentry("background");
55

6-
chrome.runtime.onInstalled.addListener(() => {
7-
console.debug("authentik Extension Installed");
8-
});
6+
const browserApi = (globalThis as typeof globalThis & { browser?: typeof chrome }).browser;
7+
const runtimeApi = browserApi?.runtime ?? chrome.runtime;
8+
9+
function stringifyError(exc: unknown): string {
10+
if (exc instanceof Error) {
11+
return exc.message;
12+
}
13+
return String(exc);
14+
}
915

1016
const native = new Native();
1117

12-
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
18+
async function handleMessage(msg: { action?: string; profile?: string; challenge?: string }) {
1319
switch (msg.action) {
1420
case "platform_sign_endpoint_header":
15-
native
16-
.platformSignEndpointHeader(msg.profile, msg.challenge)
17-
.then((r) => {
18-
sendResponse(r);
19-
})
20-
.catch((exc) => {
21-
console.warn("Failed to send request for platform sign", exc);
22-
sendResponse(null);
23-
});
24-
break;
21+
try {
22+
return await native.platformSignEndpointHeader(
23+
msg.profile ?? "default",
24+
msg.challenge ?? "",
25+
);
26+
} catch (exc) {
27+
console.warn("Failed to send request for platform sign", exc);
28+
return {
29+
error: stringifyError(exc),
30+
};
31+
}
32+
default:
33+
return false;
2534
}
26-
return true;
27-
});
35+
}
36+
37+
runtimeApi.onMessage.addListener(
38+
(
39+
msg: { action?: string; profile?: string; challenge?: string },
40+
_sender: unknown,
41+
sendResponse: (response: unknown) => void,
42+
) => {
43+
const response = handleMessage(msg);
44+
if (browserApi?.runtime) {
45+
return response;
46+
}
47+
response.then(sendResponse);
48+
return true;
49+
},
50+
);

browser-ext/src/content/content.ts

Lines changed: 63 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,41 @@
1+
function stringifyError(value: unknown): string | null {
2+
if (value && typeof value === "object" && "error" in value) {
3+
const err = (value as { error?: unknown }).error;
4+
if (typeof err === "string") {
5+
return err;
6+
}
7+
}
8+
return null;
9+
}
10+
11+
const browserApi = (globalThis as typeof globalThis & { browser?: typeof chrome }).browser;
12+
const runtimeApi = browserApi?.runtime ?? chrome.runtime;
13+
14+
function sendRuntimeMessage(message: {
15+
action: string;
16+
profile: string;
17+
challenge: string;
18+
}): Promise<unknown> {
19+
if (browserApi?.runtime) {
20+
return runtimeApi.sendMessage(message);
21+
}
22+
return new Promise((resolve, reject) => {
23+
try {
24+
runtimeApi.sendMessage(message, (response: unknown) => {
25+
const lastError =
26+
typeof chrome !== "undefined" ? chrome.runtime?.lastError : undefined;
27+
if (lastError) {
28+
reject(new Error(lastError.message));
29+
return;
30+
}
31+
resolve(response);
32+
});
33+
} catch (exc) {
34+
reject(exc);
35+
}
36+
});
37+
}
38+
139
window.addEventListener(
240
"message",
341
(event) => {
@@ -11,19 +49,34 @@ window.addEventListener(
1149
if (event.source !== window) {
1250
return;
1351
}
14-
chrome.runtime
15-
.sendMessage({
16-
action: "platform_sign_endpoint_header",
17-
profile: "default",
18-
challenge: event.data.challenge,
19-
})
52+
sendRuntimeMessage({
53+
action: "platform_sign_endpoint_header",
54+
profile: "default",
55+
challenge: event.data.challenge,
56+
})
2057
.then((signed) => {
58+
const error = stringifyError(signed);
59+
if (error) {
60+
console.warn(
61+
"authentik/bext: failed to sign endpoint challenge",
62+
error,
63+
);
64+
return;
65+
}
2166
if (signed) {
22-
window.postMessage({
23-
_ak_ext: "authentik-platform-sso",
24-
response: signed,
25-
});
67+
window.postMessage(
68+
{
69+
_ak_ext: "authentik-platform-sso",
70+
response: signed,
71+
},
72+
window.location.origin,
73+
);
74+
} else {
75+
console.warn("authentik/bext: background returned empty response");
2676
}
77+
})
78+
.catch((exc) => {
79+
console.warn("authentik/bext: background request failed", exc);
2780
});
2881
} catch (exc) {
2982
console.warn(`authentik/bext: ${exc}`);

browser-ext/src/utils/native.ts

Lines changed: 81 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,12 @@ export interface Message {
1111
export interface Response {
1212
response_to: string;
1313
data: { [key: string]: unknown };
14+
error?: string;
1415
}
1516

17+
const browserApi = (globalThis as typeof globalThis & { browser?: typeof chrome }).browser;
18+
const runtimeApi = browserApi?.runtime ?? chrome.runtime;
19+
1620
function createRandomString(length: number = 16) {
1721
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
1822
let result = "";
@@ -25,60 +29,121 @@ function createRandomString(length: number = 16) {
2529
}
2630

2731
const defaultReconnectDelay = 5;
32+
const requestTimeoutMs = 2500;
33+
34+
type PendingRequest = PromiseWithResolvers<Response> & {
35+
timeout?: ReturnType<typeof setTimeout>;
36+
};
2837

2938
export class Native {
3039
#port?: chrome.runtime.Port;
31-
#promises: Map<string, PromiseWithResolvers<Response>> = new Map();
40+
#promises: Map<string, PendingRequest> = new Map();
3241
#reconnectDelay = defaultReconnectDelay;
3342
#reconnectTimeout = 0;
43+
#isConnected = false;
3444

3545
constructor() {
3646
this.#connect();
3747
}
3848

3949
#connect() {
40-
this.#port = chrome.runtime.connectNative("io.goauthentik.platform");
41-
this.#port.onMessage.addListener(this.#listener.bind(this));
42-
this.#port.onDisconnect.addListener(() => {
50+
const port = runtimeApi.connectNative("io.goauthentik.platform");
51+
this.#port = port;
52+
this.#isConnected = true;
53+
this.#reconnectDelay = defaultReconnectDelay;
54+
port.onMessage.addListener(this.#listener.bind(this));
55+
port.onDisconnect.addListener(() => {
56+
this.#isConnected = false;
4357
this.#reconnectDelay *= 1.35;
4458
this.#reconnectDelay = Math.min(this.#reconnectDelay, 3600);
45-
// @ts-ignore
46-
const err = chrome.runtime.lastError || this.#port?.error;
47-
console.debug(
48-
`authentik/bext/native: Disconnected, reconnecting in ${this.#reconnectDelay}`,
49-
err,
59+
const err =
60+
(typeof chrome !== "undefined" ? chrome.runtime?.lastError : undefined) ||
61+
(port as chrome.runtime.Port & { error?: unknown }).error;
62+
this.#rejectPending(
63+
new Error(`native host disconnected${err ? `: ${String(err)}` : ""}`),
5064
);
65+
this.#port = undefined;
5166
clearTimeout(this.#reconnectTimeout);
5267
this.#reconnectTimeout = setTimeout(() => {
5368
this.#connect();
5469
}, this.#reconnectDelay * 1000);
5570
});
56-
console.debug("authentik/bext/native: Connected to native");
5771
}
5872

5973
#listener(msg: Response) {
6074
const prom = this.#promises.get(msg.response_to);
61-
console.debug(`authentik/bext/native[${msg.response_to}]: Got response`);
6275
if (!prom) {
63-
console.debug(`authentik/bext/native[${msg.response_to}]: No promise to resolve`);
6476
return;
6577
}
78+
if (msg.error) {
79+
if (prom.timeout) {
80+
clearTimeout(prom.timeout);
81+
}
82+
prom.reject(new Error(msg.error));
83+
this.#promises.delete(msg.response_to);
84+
return;
85+
}
86+
if (prom.timeout) {
87+
clearTimeout(prom.timeout);
88+
}
6689
prom.resolve(msg);
90+
this.#promises.delete(msg.response_to);
91+
}
92+
93+
#postMessage(msg: Message, retry: boolean) {
94+
if (!this.#port || !this.#isConnected) {
95+
this.#connect();
96+
}
97+
if (!this.#port) {
98+
throw new Error("native host is not connected");
99+
}
100+
try {
101+
this.#port.postMessage(msg);
102+
} catch (exc) {
103+
const err = exc instanceof Error ? exc.message : String(exc);
104+
if (retry && err.includes("disconnected port")) {
105+
this.#isConnected = false;
106+
this.#port = undefined;
107+
this.#connect();
108+
this.#postMessage(msg, false);
109+
return;
110+
}
111+
throw exc;
112+
}
67113
}
68114

69115
postMessage(msg: Partial<Message>): Promise<Response> {
70116
msg.id = createRandomString();
71117
const promise = Promise.withResolvers<Response>();
72118
try {
73-
this.#promises.set(msg.id, promise);
74-
this.#port?.postMessage(msg);
75-
console.debug(`authentik/bext/native[${msg.id}]: Sending message ${msg.path}`);
119+
const pending = promise as PendingRequest;
120+
pending.timeout = setTimeout(() => {
121+
this.#promises.delete(msg.id as string);
122+
pending.reject(new Error(`native host timed out after ${requestTimeoutMs}ms`));
123+
}, requestTimeoutMs);
124+
this.#promises.set(msg.id, pending);
125+
this.#postMessage(msg as Message, true);
76126
} catch (exc) {
77-
this.#promises.get(msg.id)?.reject(exc);
127+
const pending = this.#promises.get(msg.id);
128+
if (pending?.timeout) {
129+
clearTimeout(pending.timeout);
130+
}
131+
pending?.reject(exc);
132+
this.#promises.delete(msg.id);
78133
}
79134
return promise.promise;
80135
}
81136

137+
#rejectPending(error: Error) {
138+
for (const [id, pending] of this.#promises) {
139+
if (pending.timeout) {
140+
clearTimeout(pending.timeout);
141+
}
142+
pending.reject(error);
143+
this.#promises.delete(id);
144+
}
145+
}
146+
82147
async ping(): Promise<Response> {
83148
return this.postMessage({
84149
version: "1",

pkg/agent_local/tray/items.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ func (t *Tray) addProfile(name string, profile *config.ConfigV1Profile) {
6464
iat.String(),
6565
), "").Disable()
6666
i.AddSubMenuItem(fmt.Sprintf(
67-
"Renewing token in %s (%s)",
67+
"Renewing token %s (%s)",
6868
timediff.TimeDiff(exp.Time),
6969
exp.String(),
7070
), "").Disable()

0 commit comments

Comments
 (0)