Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 39 additions & 16 deletions browser-ext/src/background/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,48 @@ import { sentry } from "../utils/sentry";

sentry("background");

chrome.runtime.onInstalled.addListener(() => {
console.debug("authentik Extension Installed");
});
const browserApi = (globalThis as typeof globalThis & { browser?: typeof chrome }).browser;
const runtimeApi = browserApi?.runtime ?? chrome.runtime;

function stringifyError(exc: unknown): string {
if (exc instanceof Error) {
return exc.message;
}
return String(exc);
}

const native = new Native();

chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
async function handleMessage(msg: { action?: string; profile?: string; challenge?: string }) {
switch (msg.action) {
case "platform_sign_endpoint_header":
native
.platformSignEndpointHeader(msg.profile, msg.challenge)
.then((r) => {
sendResponse(r);
})
.catch((exc) => {
console.warn("Failed to send request for platform sign", exc);
sendResponse(null);
});
break;
try {
return await native.platformSignEndpointHeader(
msg.profile ?? "default",
msg.challenge ?? "",
);
} catch (exc) {
console.warn("Failed to send request for platform sign", exc);
return {
error: stringifyError(exc),
};
}
default:
return false;
}
return true;
});
}

runtimeApi.onMessage.addListener(
(
msg: { action?: string; profile?: string; challenge?: string },
_sender: unknown,
sendResponse: (response: unknown) => void,
) => {
const response = handleMessage(msg);
if (browserApi?.runtime) {
return response;
}
response.then(sendResponse);
return true;
},
);
73 changes: 63 additions & 10 deletions browser-ext/src/content/content.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,41 @@
function stringifyError(value: unknown): string | null {
if (value && typeof value === "object" && "error" in value) {
const err = (value as { error?: unknown }).error;
if (typeof err === "string") {
return err;
}
}
return null;
}

const browserApi = (globalThis as typeof globalThis & { browser?: typeof chrome }).browser;
const runtimeApi = browserApi?.runtime ?? chrome.runtime;

function sendRuntimeMessage(message: {
action: string;
profile: string;
challenge: string;
}): Promise<unknown> {
if (browserApi?.runtime) {
return runtimeApi.sendMessage(message);
}
return new Promise((resolve, reject) => {
try {
runtimeApi.sendMessage(message, (response: unknown) => {
const lastError =
typeof chrome !== "undefined" ? chrome.runtime?.lastError : undefined;
if (lastError) {
reject(new Error(lastError.message));
return;
}
resolve(response);
});
} catch (exc) {
reject(exc);
}
});
}

window.addEventListener(
"message",
(event) => {
Expand All @@ -11,19 +49,34 @@ window.addEventListener(
if (event.source !== window) {
return;
}
chrome.runtime
.sendMessage({
action: "platform_sign_endpoint_header",
profile: "default",
challenge: event.data.challenge,
})
sendRuntimeMessage({
action: "platform_sign_endpoint_header",
profile: "default",
challenge: event.data.challenge,
})
.then((signed) => {
const error = stringifyError(signed);
if (error) {
console.warn(
"authentik/bext: failed to sign endpoint challenge",
error,
);
return;
}
if (signed) {
window.postMessage({
_ak_ext: "authentik-platform-sso",
response: signed,
});
window.postMessage(
{
_ak_ext: "authentik-platform-sso",
response: signed,
},
window.location.origin,
);
} else {
console.warn("authentik/bext: background returned empty response");
}
})
.catch((exc) => {
console.warn("authentik/bext: background request failed", exc);
});
} catch (exc) {
console.warn(`authentik/bext: ${exc}`);
Expand Down
97 changes: 81 additions & 16 deletions browser-ext/src/utils/native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@ export interface Message {
export interface Response {
response_to: string;
data: { [key: string]: unknown };
error?: string;
}

const browserApi = (globalThis as typeof globalThis & { browser?: typeof chrome }).browser;
const runtimeApi = browserApi?.runtime ?? chrome.runtime;

function createRandomString(length: number = 16) {
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let result = "";
Expand All @@ -25,60 +29,121 @@ function createRandomString(length: number = 16) {
}

const defaultReconnectDelay = 5;
const requestTimeoutMs = 2500;

type PendingRequest = PromiseWithResolvers<Response> & {
timeout?: ReturnType<typeof setTimeout>;
};

export class Native {
#port?: chrome.runtime.Port;
#promises: Map<string, PromiseWithResolvers<Response>> = new Map();
#promises: Map<string, PendingRequest> = new Map();
#reconnectDelay = defaultReconnectDelay;
#reconnectTimeout = 0;
#isConnected = false;

constructor() {
this.#connect();
}

#connect() {
this.#port = chrome.runtime.connectNative("io.goauthentik.platform");
this.#port.onMessage.addListener(this.#listener.bind(this));
this.#port.onDisconnect.addListener(() => {
const port = runtimeApi.connectNative("io.goauthentik.platform");
this.#port = port;
this.#isConnected = true;
this.#reconnectDelay = defaultReconnectDelay;
port.onMessage.addListener(this.#listener.bind(this));
port.onDisconnect.addListener(() => {
this.#isConnected = false;
this.#reconnectDelay *= 1.35;
this.#reconnectDelay = Math.min(this.#reconnectDelay, 3600);
// @ts-ignore
const err = chrome.runtime.lastError || this.#port?.error;
console.debug(
`authentik/bext/native: Disconnected, reconnecting in ${this.#reconnectDelay}`,
err,
const err =
(typeof chrome !== "undefined" ? chrome.runtime?.lastError : undefined) ||
(port as chrome.runtime.Port & { error?: unknown }).error;
this.#rejectPending(
new Error(`native host disconnected${err ? `: ${String(err)}` : ""}`),
);
this.#port = undefined;
clearTimeout(this.#reconnectTimeout);
this.#reconnectTimeout = setTimeout(() => {
this.#connect();
}, this.#reconnectDelay * 1000);
});
console.debug("authentik/bext/native: Connected to native");
}

#listener(msg: Response) {
const prom = this.#promises.get(msg.response_to);
console.debug(`authentik/bext/native[${msg.response_to}]: Got response`);
if (!prom) {
console.debug(`authentik/bext/native[${msg.response_to}]: No promise to resolve`);
return;
}
if (msg.error) {
if (prom.timeout) {
clearTimeout(prom.timeout);
}
prom.reject(new Error(msg.error));
this.#promises.delete(msg.response_to);
return;
}
if (prom.timeout) {
clearTimeout(prom.timeout);
}
prom.resolve(msg);
this.#promises.delete(msg.response_to);
}

#postMessage(msg: Message, retry: boolean) {
if (!this.#port || !this.#isConnected) {
this.#connect();
}
if (!this.#port) {
throw new Error("native host is not connected");
}
try {
this.#port.postMessage(msg);
} catch (exc) {
const err = exc instanceof Error ? exc.message : String(exc);
if (retry && err.includes("disconnected port")) {
this.#isConnected = false;
this.#port = undefined;
this.#connect();
this.#postMessage(msg, false);
return;
}
throw exc;
}
}

postMessage(msg: Partial<Message>): Promise<Response> {
msg.id = createRandomString();
const promise = Promise.withResolvers<Response>();
try {
this.#promises.set(msg.id, promise);
this.#port?.postMessage(msg);
console.debug(`authentik/bext/native[${msg.id}]: Sending message ${msg.path}`);
const pending = promise as PendingRequest;
pending.timeout = setTimeout(() => {
this.#promises.delete(msg.id as string);
pending.reject(new Error(`native host timed out after ${requestTimeoutMs}ms`));
}, requestTimeoutMs);
this.#promises.set(msg.id, pending);
this.#postMessage(msg as Message, true);
} catch (exc) {
this.#promises.get(msg.id)?.reject(exc);
const pending = this.#promises.get(msg.id);
if (pending?.timeout) {
clearTimeout(pending.timeout);
}
pending?.reject(exc);
this.#promises.delete(msg.id);
}
return promise.promise;
}

#rejectPending(error: Error) {
for (const [id, pending] of this.#promises) {
if (pending.timeout) {
clearTimeout(pending.timeout);
}
pending.reject(error);
this.#promises.delete(id);
}
}

async ping(): Promise<Response> {
return this.postMessage({
version: "1",
Expand Down
2 changes: 1 addition & 1 deletion pkg/agent_local/tray/items.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func (t *Tray) addProfile(name string, profile *config.ConfigV1Profile) {
iat.String(),
), "").Disable()
i.AddSubMenuItem(fmt.Sprintf(
"Renewing token in %s (%s)",
"Renewing token %s (%s)",
timediff.TimeDiff(exp.Time),
exp.String(),
), "").Disable()
Expand Down
Loading