diff --git a/.github/actions/apt/action.yml b/.github/actions/apt/action.yml
index bb8c485a..3e29c8f1 100644
--- a/.github/actions/apt/action.yml
+++ b/.github/actions/apt/action.yml
@@ -191,6 +191,19 @@ runs:
Content-Type: application/vnd.debian.binary-package
Cache-Control: public, max-age=86400
+ # Nix binary cache headers
+ /nix/nix-cache-info
+ Content-Type: text/plain
+ Cache-Control: public, max-age=300
+
+ /nix/*.narinfo
+ Content-Type: text/x-nix-narinfo
+ Cache-Control: public, max-age=3600
+
+ /nix/nar/*
+ Content-Type: application/x-nix-archive
+ Cache-Control: public, max-age=604800
+
# Security headers
/*
X-Frame-Options: DENY
diff --git a/.github/actions/nix/action.yml b/.github/actions/nix/action.yml
new file mode 100644
index 00000000..8bce04fc
--- /dev/null
+++ b/.github/actions/nix/action.yml
@@ -0,0 +1,161 @@
+name: 'Build Nix Packages'
+description: 'Builds Nix packages and creates a binary cache'
+
+inputs:
+ signing-key:
+ description: 'Nix signing private key (plain text)'
+ required: false
+ output-path:
+ description: 'Output path for binary cache'
+ required: false
+ default: './nix-cache'
+ packages:
+ description: 'Space-separated list of cross-platform packages to build'
+ required: false
+ default: 'ak-cli ak-agent ak-browser-support'
+ linux-only-packages:
+ description: 'Space-separated list of Linux-only packages to build'
+ required: false
+ default: 'ak-sysd libpam-authentik libnss-authentik'
+
+outputs:
+ cache-path:
+ description: 'Path to generated binary cache'
+ value: ${{ steps.build.outputs.cache-path }}
+ cache-size:
+ description: 'Size of the generated cache'
+ value: ${{ steps.stats.outputs.size }}
+
+runs:
+ using: 'composite'
+ steps:
+ - name: Setup output directory
+ id: setup
+ shell: bash
+ run: |
+ set -xeuo pipefail
+ mkdir -p "${{ inputs.output-path }}/nar"
+ CACHE_PATH=$(realpath "${{ inputs.output-path }}")
+ echo "cache-path=$CACHE_PATH" >> $GITHUB_OUTPUT
+ echo "CACHE_PATH=$CACHE_PATH" >> $GITHUB_ENV
+
+ - name: Setup signing key
+ if: ${{ inputs.signing-key != '' }}
+ shell: bash
+ run: |
+ set -xeuo pipefail
+ SIGNING_KEY_FILE="${{ runner.temp }}/nix-signing-key.pem"
+ echo "${{ inputs.signing-key }}" > "$SIGNING_KEY_FILE"
+ chmod 600 "$SIGNING_KEY_FILE"
+ echo "SIGNING_KEY_FILE=$SIGNING_KEY_FILE" >> $GITHUB_ENV
+
+ - name: Detect platform
+ id: platform
+ shell: bash
+ run: |
+ set -xeuo pipefail
+ case "$(uname -s)" in
+ Linux) PLATFORM="linux" ;;
+ Darwin) PLATFORM="darwin" ;;
+ *) PLATFORM="unknown" ;;
+ esac
+ echo "platform=$PLATFORM" >> $GITHUB_OUTPUT
+
+ - name: Build packages
+ id: build
+ shell: bash
+ run: |
+ set -xeuo pipefail
+
+ PACKAGES="${{ inputs.packages }}"
+ LINUX_ONLY_PACKAGES="${{ inputs.linux-only-packages }}"
+
+ # Build cross-platform packages
+ for pkg in $PACKAGES; do
+ echo "Building $pkg..."
+ nix build ".#$pkg" --no-link --print-out-paths
+ done
+
+ # Build Linux-only packages
+ if [ "${{ steps.platform.outputs.platform }}" = "linux" ]; then
+ for pkg in $LINUX_ONLY_PACKAGES; do
+ echo "Building $pkg..."
+ nix build ".#$pkg" --no-link --print-out-paths
+ done
+ fi
+
+ echo "cache-path=$CACHE_PATH" >> $GITHUB_OUTPUT
+
+ - name: Copy to binary cache
+ shell: bash
+ run: |
+ set -xeuo pipefail
+
+ PACKAGES="${{ inputs.packages }}"
+ LINUX_ONLY_PACKAGES="${{ inputs.linux-only-packages }}"
+
+ # Determine signing args
+ SIGN_ARGS=""
+ if [ -n "${SIGNING_KEY_FILE:-}" ]; then
+ SIGN_ARGS="--secret-key-files $SIGNING_KEY_FILE"
+ fi
+
+ # Copy cross-platform packages
+ for pkg in $PACKAGES; do
+ echo "Copying $pkg to cache..."
+ nix copy --to "file://$CACHE_PATH" ".#$pkg" $SIGN_ARGS
+ done
+
+ # Copy Linux-only packages
+ if [ "${{ steps.platform.outputs.platform }}" = "linux" ]; then
+ for pkg in $LINUX_ONLY_PACKAGES; do
+ echo "Copying $pkg to cache..."
+ nix copy --to "file://$CACHE_PATH" ".#$pkg" $SIGN_ARGS
+ done
+ fi
+
+ - name: Create cache metadata
+ shell: bash
+ run: |
+ set -xeuo pipefail
+ cat > "$CACHE_PATH/nix-cache-info" << EOF
+ StoreDir: /nix/store
+ WantMassQuery: 1
+ Priority: 40
+ EOF
+
+ - name: Generate cache statistics
+ id: stats
+ shell: bash
+ run: |
+ set -xeuo pipefail
+ cd "$CACHE_PATH"
+
+ size=$(du -sh . | cut -f1)
+ echo "Cache size: $size"
+ echo "size=$size" >> $GITHUB_OUTPUT
+
+ # Count files
+ narinfo_count=$(find . -name "*.narinfo" 2>/dev/null | wc -l | tr -d ' ')
+ nar_count=$(find ./nar -name "*.nar*" 2>/dev/null | wc -l | tr -d ' ')
+ echo "NAR info files: $narinfo_count"
+ echo "NAR archives: $nar_count"
+ echo "narinfo-count=$narinfo_count" >> $GITHUB_OUTPUT
+ echo "nar-count=$nar_count" >> $GITHUB_OUTPUT
+
+ echo "Cache structure:"
+ find . -type f | head -20
+
+ - name: Validate cache structure
+ shell: bash
+ run: |
+ set -xeuo pipefail
+ cd "$CACHE_PATH"
+
+ # Check required files exist
+ if [ ! -f "nix-cache-info" ]; then
+ echo "Error: nix-cache-info missing"
+ exit 1
+ fi
+
+ echo "Cache validation passed"
diff --git a/.github/actions/repo/index.html b/.github/actions/repo/index.html
index 56091f27..8187cdb3 100644
--- a/.github/actions/repo/index.html
+++ b/.github/actions/repo/index.html
@@ -57,6 +57,67 @@
+
Nix/NixOS Setup
+
+
Option 1: Direct Installation
+
# Install with binary cache
+nix profile install github:goauthentik/platform#ak-cli \
+ --extra-substituters "${REPOSITORY_URL}nix" \
+ --extra-trusted-public-keys "authentik-pkg:ZZHUD/9SkS8T1BVVoksE/+QjIo0s3F8/AM/h0J3ckaw="
+
+# Available packages: ak-cli, ak-sysd, ak-agent, ak-browser-support
+# Linux only: libpam-authentik, libnss-authentik
+
+
+
Option 2: Permanent Cache Configuration
+
Add to /etc/nix/nix.conf or ~/.config/nix/nix.conf:
+
extra-substituters = ${REPOSITORY_URL}nix
+extra-trusted-public-keys = authentik-pkg:ZZHUD/9SkS8T1BVVoksE/+QjIo0s3F8/AM/h0J3ckaw=
+
Then install normally:
+
nix profile install github:goauthentik/platform#ak-cli
+
+
+
Option 3: NixOS Configuration
+
# In your flake.nix inputs:
+authentik.url = "github:goauthentik/platform";
+
+# In your NixOS configuration:
+{ authentik, ... }: {
+ imports = [ authentik.nixosModules.default ];
+ services.authentik = {
+ enable = true;
+ enablePAM = true;
+ enableNSS = true;
+ };
+}
+
+
+
Option 4: nix-darwin Configuration (macOS)
+
# In your flake.nix inputs:
+authentik.url = "github:goauthentik/platform";
+
+# In your darwin configuration:
+{ authentik, ... }: {
+ imports = [ authentik.darwinModules.default ];
+ nixpkgs.overlays = [ authentik.overlays.default ];
+ services.authentik.enable = true;
+}
+
This installs the .app bundle to /Applications/ and configures launchd to run ak-sysd.
+
+
+
Available Nix Packages
+
+ | Package | Description | Platforms |
+ ak-cli | Command-line interface | Linux, macOS |
+ ak-sysd | System daemon | Linux, macOS |
+ ak-agent | Local agent (includes all binaries on macOS) | Linux, macOS |
+ ak-browser-support | Browser extension support | Linux, macOS |
+ libpam-authentik | PAM authentication module | Linux |
+ libnss-authentik | NSS name resolution module | Linux |
+
+
+
+
Repository Information
Built from: ${REF} (${SHA})
GPG Key fingerprint: 82EE AAD5 531A 856A 9C72 6132 2217 2AF2 AAE3 A237
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index e88c32b3..ad554182 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -132,11 +132,41 @@ jobs:
with:
name: authentik_${{ matrix.platform }}_${{ matrix.target }}
path: bin/
+ build_nix:
+ name: Build (Nix)
+ strategy:
+ fail-fast: false
+ matrix:
+ platform:
+ - ubuntu-24.04
+ - ubuntu-24.04-arm
+ - macos-15
+ runs-on: ${{ matrix.platform }}
+ steps:
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
+ - uses: DeterminateSystems/nix-installer-action@v21
+ - uses: DeterminateSystems/magic-nix-cache-action@v13
+ - uses: ./.github/actions/nix
+ id: nix-build
+ with:
+ signing-key: ${{ secrets.NIX_SIGNING_KEY }}
+ output-path: ./nix-cache
+ - name: Show build info
+ shell: bash
+ run: |
+ echo "Cache size: $NIX_CACHE_SIZE"
+ env:
+ NIX_CACHE_SIZE: ${{ steps.nix-build.outputs.cache-size }}
+ - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v5
+ with:
+ name: nix-cache-${{ matrix.platform }}
+ path: ./nix-cache/
deploy:
needs:
- build_go
- build_js
- build_rs
+ - build_nix
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
@@ -150,6 +180,37 @@ jobs:
with:
package-path: bin/
gpg-private-key: "${{ secrets.GPG_PRIVATE }}"
+ - name: Merge Nix caches
+ shell: bash
+ run: |
+ set -xeuo pipefail
+ mkdir -p ./repo/nix/nar
+
+ # Merge all platform caches
+ for cache_dir in ./bin/nix-cache-*/; do
+ if [ -d "$cache_dir" ]; then
+ echo "Merging cache from: $cache_dir"
+ # Copy narinfo files (don't overwrite existing)
+ find "$cache_dir" -maxdepth 1 -name "*.narinfo" -exec cp -n {} ./repo/nix/ \; 2>/dev/null || true
+ # Copy nar archives
+ if [ -d "$cache_dir/nar" ]; then
+ find "$cache_dir/nar" -name "*.nar*" -exec cp -n {} ./repo/nix/nar/ \; 2>/dev/null || true
+ fi
+ fi
+ done
+
+ # Create unified nix-cache-info
+ cat > ./repo/nix/nix-cache-info << EOF
+ StoreDir: /nix/store
+ WantMassQuery: 1
+ Priority: 40
+ EOF
+
+ # Show cache stats
+ echo "Merged Nix cache:"
+ du -sh ./repo/nix
+ echo "NAR info files: $(find ./repo/nix -maxdepth 1 -name '*.narinfo' | wc -l)"
+ echo "NAR archives: $(find ./repo/nix/nar -name '*.nar*' 2>/dev/null | wc -l)"
- name: Upload repository
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v5
with:
@@ -171,12 +232,16 @@ jobs:
steps:
- name: Determine component name
id: component
+ shell: bash
run: |
- if [ "${{ github.event_name }}" = "pull_request" ]; then
- echo "component=pr-${{ github.event.number }}" >> $GITHUB_OUTPUT
+ if [ "$EVENT_NAME" = "pull_request" ]; then
+ echo "component=pr-$EVENT_NUMBER" >> $GITHUB_OUTPUT
else
echo "component=main" >> $GITHUB_OUTPUT
fi
+ env:
+ EVENT_NAME: ${{ github.event_name }}
+ EVENT_NUMBER: ${{ github.event.number }}
- name: Download repository
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v6
with:
@@ -187,6 +252,11 @@ jobs:
with:
name: all-packages
path: ./deploy/packages
+ - name: Show deployment structure
+ shell: bash
+ run: |
+ echo "APT repo size: $(du -sh ./deploy/dists 2>/dev/null | cut -f1 || echo 'N/A')"
+ echo "Nix cache size: $(du -sh ./deploy/nix 2>/dev/null | cut -f1 || echo 'N/A')"
- id: app-token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
with:
diff --git a/.gitignore b/.gitignore
index 00108531..266343ea 100644
--- a/.gitignore
+++ b/.gitignore
@@ -106,3 +106,6 @@ target/
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
+
+# Nix symlink
+result
diff --git a/Makefile b/Makefile
index f67517d5..32e30552 100644
--- a/Makefile
+++ b/Makefile
@@ -115,8 +115,12 @@ test-full: clean agent/test-deploy sysd/test-deploy cli/test-deploy nss/test-dep
dev--initialize: containers/test/local-build
bump:
- sed -i 's/VERSION = .*/VERSION = ${version}/g' common.mk
- sed -i 's/^version = ".*"/version = "${version}"/g' ${TOP}/Cargo.toml
+ sed -i '' 's/VERSION = .*/VERSION = ${version}/g' common.mk 2>/dev/null || \
+ sed -i 's/VERSION = .*/VERSION = ${version}/g' common.mk
+ sed -i '' 's/^version = ".*"/version = "${version}"/g' ${TOP}/Cargo.toml 2>/dev/null || \
+ sed -i 's/^version = ".*"/version = "${version}"/g' ${TOP}/Cargo.toml
+ sed -i '' 's/version = "[0-9]*\.[0-9]*\.[0-9]*";/version = "${version}";/' ${TOP}/flake.nix 2>/dev/null || \
+ sed -i 's/version = "[0-9]*\.[0-9]*\.[0-9]*";/version = "${version}";/' ${TOP}/flake.nix
"$(MAKE)" browser-ext/bump
"$(MAKE)" agent/bump
"$(MAKE)" nss/bump
@@ -159,3 +163,65 @@ containers/test/%:
containers/e2e/%:
"$(MAKE)" -C "${TOP}/containers/e2e" $*
+
+# Nix targets
+.PHONY: nix-build
+nix-build:
+ nix build .#ak-cli
+ nix build .#ak-agent
+ nix build .#ak-browser-support
+ifeq ($(shell uname -s),Linux)
+ nix build .#ak-sysd
+ nix build .#libpam-authentik
+ nix build .#libnss-authentik
+endif
+
+.PHONY: nix-build-go
+nix-build-go:
+ nix build .#ak-cli
+ nix build .#ak-agent
+ nix build .#ak-browser-support
+ifeq ($(shell uname -s),Linux)
+ nix build .#ak-sysd
+endif
+
+.PHONY: nix-develop
+nix-develop:
+ nix develop
+
+.PHONY: nix-check
+nix-check:
+ nix flake check
+
+.PHONY: nix-update
+nix-update:
+ nix flake update
+
+.PHONY: nix-update-vendor-hash
+nix-update-vendor-hash:
+ @echo "Updating Go vendor hash in flake.nix..."
+ sed -i '' 's/vendorHash = "sha256-.*";/vendorHash = "";/' ${TOP}/flake.nix 2>/dev/null || \
+ sed -i 's/vendorHash = "sha256-.*";/vendorHash = "";/' ${TOP}/flake.nix
+ @NEW_HASH=$$(nix build .#ak-cli 2>&1 | grep -o 'got:[[:space:]]*sha256-[A-Za-z0-9+/=]*' | sed 's/got:[[:space:]]*//' | head -1) && \
+ if [ -n "$$NEW_HASH" ]; then \
+ sed -i '' "s/vendorHash = \"\";/vendorHash = \"$$NEW_HASH\";/" ${TOP}/flake.nix 2>/dev/null || \
+ sed -i "s/vendorHash = \"\";/vendorHash = \"$$NEW_HASH\";/" ${TOP}/flake.nix; \
+ echo "Updated vendorHash to: $$NEW_HASH"; \
+ else \
+ echo "Failed to extract hash. Check nix build output manually."; \
+ exit 1; \
+ fi
+
+.PHONY: nix-cache
+nix-cache:
+ mkdir -p ${TOP}/bin/nix-cache/nar
+ nix copy --to "file://${TOP}/bin/nix-cache" .#ak-cli
+ nix copy --to "file://${TOP}/bin/nix-cache" .#ak-agent
+ nix copy --to "file://${TOP}/bin/nix-cache" .#ak-browser-support
+ifeq ($(shell uname -s),Linux)
+ nix copy --to "file://${TOP}/bin/nix-cache" .#ak-sysd
+ nix copy --to "file://${TOP}/bin/nix-cache" .#libpam-authentik
+ nix copy --to "file://${TOP}/bin/nix-cache" .#libnss-authentik
+endif
+ echo "StoreDir: /nix/store" > ${TOP}/bin/nix-cache/nix-cache-info
+ echo "WantMassQuery: 1" >> ${TOP}/bin/nix-cache/nix-cache-info
diff --git a/browser-ext/src/background/background.ts b/browser-ext/src/background/background.ts
index 5ec28df0..2c3822f9 100644
--- a/browser-ext/src/background/background.ts
+++ b/browser-ext/src/background/background.ts
@@ -3,25 +3,45 @@ 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;
+ }
+}
+
+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;
-});
+ },
+);
diff --git a/browser-ext/src/content/content.ts b/browser-ext/src/content/content.ts
index 28d3d65a..1e0df467 100644
--- a/browser-ext/src/content/content.ts
+++ b/browser-ext/src/content/content.ts
@@ -1,3 +1,57 @@
+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
{
+ return new Promise((resolve, reject) => {
+ let settled = false;
+ const finish = (fn: (value: unknown) => void, value: unknown) => {
+ if (settled) {
+ return;
+ }
+ settled = true;
+ fn(value);
+ };
+ try {
+ const maybePromise = runtimeApi.sendMessage(message, (response: unknown) => {
+ const lastError =
+ typeof chrome !== "undefined" ? chrome.runtime?.lastError : undefined;
+ if (lastError) {
+ finish(reject, new Error(lastError.message));
+ return;
+ }
+ finish(resolve, response);
+ }) as unknown;
+ if (
+ maybePromise &&
+ typeof maybePromise === "object" &&
+ "then" in maybePromise &&
+ typeof maybePromise.then === "function"
+ ) {
+ maybePromise.then(
+ (response: unknown) => finish(resolve, response),
+ (error: unknown) => finish(reject, error),
+ );
+ }
+ } catch (exc) {
+ finish(reject, exc);
+ }
+ });
+}
+
window.addEventListener(
"message",
(event) => {
@@ -11,19 +65,31 @@ window.addEventListener(
if (event.source !== window) {
return;
}
- chrome.runtime
- .sendMessage({
+ 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.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}`);
diff --git a/browser-ext/src/utils/native.ts b/browser-ext/src/utils/native.ts
index d5a8ee4d..36ec799d 100644
--- a/browser-ext/src/utils/native.ts
+++ b/browser-ext/src/utils/native.ts
@@ -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 = "";
@@ -25,60 +29,125 @@ function createRandomString(length: number = 16) {
}
const defaultReconnectDelay = 5;
+const requestTimeoutMs = 2500;
+
+type PendingRequest = PromiseWithResolvers & {
+ timeout?: ReturnType;
+};
export class Native {
#port?: chrome.runtime.Port;
- #promises: Map> = new Map();
+ #promises: Map = 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): Promise {
msg.id = createRandomString();
const promise = Promise.withResolvers();
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 {
return this.postMessage({
version: "1",
diff --git a/flake.lock b/flake.lock
new file mode 100644
index 00000000..56c7a564
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,82 @@
+{
+ "nodes": {
+ "flake-utils": {
+ "inputs": {
+ "systems": "systems"
+ },
+ "locked": {
+ "lastModified": 1731533236,
+ "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
+ "type": "github"
+ },
+ "original": {
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "type": "github"
+ }
+ },
+ "nixpkgs": {
+ "locked": {
+ "lastModified": 1772736753,
+ "narHash": "sha256-au/m3+EuBLoSzWUCb64a/MZq6QUtOV8oC0D9tY2scPQ=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "917fec990948658ef1ccd07cef2a1ef060786846",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "ref": "nixpkgs-unstable",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "root": {
+ "inputs": {
+ "flake-utils": "flake-utils",
+ "nixpkgs": "nixpkgs",
+ "rust-overlay": "rust-overlay"
+ }
+ },
+ "rust-overlay": {
+ "inputs": {
+ "nixpkgs": [
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1772852295,
+ "narHash": "sha256-3FB/WzLZSiU2Mc50C9q9VXU1LRUZbsU6UHKmZG1C+hU=",
+ "owner": "oxalica",
+ "repo": "rust-overlay",
+ "rev": "c10801f59c68e14c308aea8fa6b0b3d81d43c61e",
+ "type": "github"
+ },
+ "original": {
+ "owner": "oxalica",
+ "repo": "rust-overlay",
+ "type": "github"
+ }
+ },
+ "systems": {
+ "locked": {
+ "lastModified": 1681028828,
+ "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+ "owner": "nix-systems",
+ "repo": "default",
+ "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nix-systems",
+ "repo": "default",
+ "type": "github"
+ }
+ }
+ },
+ "root": "root",
+ "version": 7
+}
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 00000000..f3664c6b
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,139 @@
+{
+ description = "authentik platform";
+
+ nixConfig = {
+ extra-substituters = [ "https://pr-525--authentik-pkg.netlify.app/nix" ];
+ extra-trusted-public-keys = [
+ "authentik-pkg:ZZHUD/9SkS8T1BVVoksE/+QjIo0s3F8/AM/h0J3ckaw="
+ ];
+ };
+
+ inputs = {
+ nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
+ flake-utils.url = "github:numtide/flake-utils";
+ rust-overlay = {
+ url = "github:oxalica/rust-overlay";
+ inputs.nixpkgs.follows = "nixpkgs";
+ };
+ };
+
+ outputs = { self, nixpkgs, flake-utils, rust-overlay }:
+ let
+ systems = [
+ "x86_64-linux"
+ "aarch64-linux"
+ "x86_64-darwin"
+ "aarch64-darwin"
+ ];
+ cargoVersion = (builtins.fromTOML (builtins.readFile ./Cargo.toml)).package.version;
+ version = "0.40.5";
+ vendorHash = "sha256-RosEV9rlG46zvHQVmC64haOcZZo5b/LWu0EgN0IEtdA=";
+ mkOverlay = final: prev:
+ let
+ system = prev.stdenv.hostPlatform.system;
+ in
+ {
+ authentik-cli = self.packages.${system}.ak-cli;
+ authentik-sysd = self.packages.${system}.ak-sysd;
+ authentik-agent = self.packages.${system}.ak-agent;
+ authentik-browser-support = self.packages.${system}.ak-browser-support;
+ } // prev.lib.optionalAttrs prev.stdenv.isLinux {
+ authentik-pam = self.packages.${system}.libpam-authentik;
+ authentik-nss = self.packages.${system}.libnss-authentik;
+ };
+ in
+ assert version == cargoVersion;
+ flake-utils.lib.eachSystem systems
+ (system:
+ let
+ pkgs = import nixpkgs {
+ inherit system;
+ overlays = [ rust-overlay.overlays.default ];
+ };
+ lib = pkgs.lib;
+ rustToolchain = pkgs.rust-bin.stable.latest.default.override {
+ extensions = [ "rust-src" ];
+ };
+ buildHash = if self ? rev then self.rev else "dirty";
+ commonGoArgs = {
+ buildGoModule = pkgs.buildGo126Module;
+ inherit version vendorHash;
+ goBuildInputs = with pkgs; [
+ pkg-config
+ ] ++ lib.optionals pkgs.stdenv.isDarwin [
+ libiconv
+ ];
+ goLdflags = [
+ "-s"
+ "-w"
+ "-X goauthentik.io/platform/pkg/meta.Version=${version}"
+ "-X goauthentik.io/platform/pkg/meta.BuildHash=${buildHash}"
+ ];
+ };
+ mkGoPackage = path: pkgs.callPackage path commonGoArgs;
+ basePackages = rec {
+ ak-cli = mkGoPackage ./nix/packages/ak-cli.nix;
+ ak-sysd = mkGoPackage ./nix/packages/ak-sysd.nix;
+ ak-agent = mkGoPackage ./nix/packages/ak-agent.nix;
+ ak-browser-support = mkGoPackage ./nix/packages/ak-browser-support.nix;
+ default = ak-agent;
+ };
+ linuxPackages = lib.optionalAttrs pkgs.stdenv.isLinux {
+ libpam-authentik = pkgs.callPackage ./nix/packages/libpam-authentik.nix {
+ inherit version;
+ };
+ libnss-authentik = pkgs.callPackage ./nix/packages/libnss-authentik.nix {
+ inherit version;
+ };
+ };
+ packages = basePackages // linuxPackages;
+ in
+ {
+ inherit packages;
+
+ apps.default = {
+ type = "app";
+ program = "${packages.default}/bin/ak-agent";
+ };
+
+ checks = packages;
+
+ devShells.default = pkgs.mkShell {
+ buildInputs = with pkgs; [
+ go_1_26
+ gopls
+ gotools
+ go-tools
+ rustToolchain
+ rust-analyzer
+ protobuf
+ protoc-gen-go
+ protoc-gen-go-grpc
+ pkg-config
+ openssl
+ nodejs
+ gnumake
+ ] ++ lib.optionals pkgs.stdenv.isLinux [
+ pam
+ ] ++ lib.optionals pkgs.stdenv.isDarwin [
+ libiconv
+ ];
+
+ shellHook = ''
+ echo "authentik platform development shell"
+ echo "Go: $(go version)"
+ echo "Rust: $(rustc --version)"
+ '';
+ };
+
+ formatter = pkgs.nixpkgs-fmt;
+ }) // {
+ nixosModules.default = import ./nix/modules/authentik.nix;
+ nixosModules.authentik = self.nixosModules.default;
+
+ darwinModules.default = import ./nix/modules/darwin.nix;
+ darwinModules.authentik = self.darwinModules.default;
+
+ overlays.default = mkOverlay;
+ };
+}
diff --git a/nix/modules/authentik.nix b/nix/modules/authentik.nix
new file mode 100644
index 00000000..8fb94745
--- /dev/null
+++ b/nix/modules/authentik.nix
@@ -0,0 +1,155 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+ cfg = config.services.authentik;
+in {
+ options.services.authentik = {
+ enable = mkEnableOption "authentik platform agent";
+
+ package = mkOption {
+ type = types.package;
+ default = pkgs.authentik-sysd or (throw "authentik-sysd package not found. Add the authentik overlay to your nixpkgs.");
+ defaultText = literalExpression "pkgs.authentik-sysd";
+ description = "The authentik sysd package to use.";
+ };
+
+ cliPackage = mkOption {
+ type = types.package;
+ default = pkgs.authentik-cli or (throw "authentik-cli package not found. Add the authentik overlay to your nixpkgs.");
+ defaultText = literalExpression "pkgs.authentik-cli";
+ description = "The authentik CLI package to use.";
+ };
+
+ agentPackage = mkOption {
+ type = types.package;
+ default = pkgs.authentik-agent or (throw "authentik-agent package not found. Add the authentik overlay to your nixpkgs.");
+ defaultText = literalExpression "pkgs.authentik-agent";
+ description = "The authentik agent package to use.";
+ };
+
+ domain = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ description = "The authentik server domain to connect to.";
+ };
+
+ configFile = mkOption {
+ type = types.nullOr types.path;
+ default = null;
+ description = "Path to the authentik configuration file.";
+ };
+
+ enablePAM = mkOption {
+ type = types.bool;
+ default = false;
+ description = "Enable PAM authentication via authentik.";
+ };
+
+ pamPackage = mkOption {
+ type = types.package;
+ default = pkgs.authentik-pam or (throw "authentik-pam package not found. Add the authentik overlay to your nixpkgs.");
+ defaultText = literalExpression "pkgs.authentik-pam";
+ description = "The authentik PAM module package to use.";
+ };
+
+ pamServices = mkOption {
+ type = types.listOf types.str;
+ default = [ "login" "sshd" "sudo" ];
+ description = "List of PAM services to enable authentik authentication for.";
+ };
+
+ enableNSS = mkOption {
+ type = types.bool;
+ default = false;
+ description = "Enable NSS name resolution via authentik.";
+ };
+
+ nssPackage = mkOption {
+ type = types.package;
+ default = pkgs.authentik-nss or (throw "authentik-nss package not found. Add the authentik overlay to your nixpkgs.");
+ defaultText = literalExpression "pkgs.authentik-nss";
+ description = "The authentik NSS module package to use.";
+ };
+
+ extraArgs = mkOption {
+ type = types.listOf types.str;
+ default = [ ];
+ description = "Extra command-line arguments to pass to ak-sysd.";
+ };
+ };
+
+ config = mkIf cfg.enable {
+ # Install CLI and agent packages
+ environment.systemPackages = [
+ cfg.cliPackage
+ cfg.agentPackage
+ ];
+
+ # Create configuration directory
+ environment.etc."authentik/.keep".text = "";
+
+ # Systemd service for ak-sysd
+ systemd.services.ak-sysd = {
+ description = "authentik System Agent";
+ after = [ "network.target" ];
+ wantedBy = [ "multi-user.target" ];
+
+ serviceConfig = {
+ Type = "simple";
+ ExecStart = "${cfg.package}/bin/ak-sysd agent ${concatStringsSep " " cfg.extraArgs}";
+ Restart = "always";
+ RestartSec = 5;
+ RuntimeDirectory = "authentik";
+ RuntimeDirectoryMode = "0777";
+ StateDirectory = "authentik";
+ ConfigurationDirectory = "authentik";
+ };
+
+ environment = mkIf (cfg.domain != null) {
+ AUTHENTIK_DOMAIN = cfg.domain;
+ };
+ };
+
+ # User systemd service for ak-agent (optional, user must enable)
+ systemd.user.services.ak-agent = {
+ description = "authentik Local Agent";
+ after = [ "graphical-session.target" ];
+ wantedBy = [ "default.target" ];
+
+ serviceConfig = {
+ Type = "simple";
+ ExecStart = "${cfg.agentPackage}/bin/ak-agent";
+ Restart = "always";
+ RestartSec = 5;
+ };
+ };
+
+ # PAM configuration
+ security.pam.services = mkIf cfg.enablePAM (
+ genAttrs cfg.pamServices (service: {
+ text = mkAfter ''
+ # authentik PAM authentication
+ auth sufficient ${cfg.pamPackage}/lib/security/pam_authentik.so
+ session required ${cfg.pamPackage}/lib/security/pam_authentik.so
+ '';
+ })
+ );
+
+ # NSS configuration
+ system.nssModules = mkIf cfg.enableNSS [ cfg.nssPackage ];
+ system.nssDatabases.passwd = mkIf cfg.enableNSS [ "authentik" ];
+ system.nssDatabases.group = mkIf cfg.enableNSS [ "authentik" ];
+
+ # Add NSS library to the dynamic linker path
+ environment.etc."ld.so.conf.d/authentik-nss.conf" = mkIf cfg.enableNSS {
+ text = "${cfg.nssPackage}/lib";
+ };
+
+ # Ensure ldconfig is run after configuration changes
+ system.activationScripts.authentik-ldconfig = mkIf cfg.enableNSS ''
+ ${pkgs.glibc.bin}/bin/ldconfig
+ '';
+ };
+}
diff --git a/nix/modules/darwin.nix b/nix/modules/darwin.nix
new file mode 100644
index 00000000..b786141b
--- /dev/null
+++ b/nix/modules/darwin.nix
@@ -0,0 +1,123 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+ cfg = config.services.authentik;
+ appPath = "/Applications/authentik Agent.app";
+ packageAppPath = "${cfg.package}/Applications/authentik Agent.app";
+ packageResourcesPath = "${packageAppPath}/Contents/Resources";
+ browserSupportPath = "${packageAppPath}/Contents/MacOS/ak-browser-support";
+ defaultConfigPath = "/opt/authentik/config/config.json";
+ targetConfigPath = if cfg.configFile != null then toString cfg.configFile else defaultConfigPath;
+ defaultSysdConfig = ../../cmd/agent_local/package/macos/scripts/sysd-config.json;
+in
+{
+ options.services.authentik = {
+ enable = mkEnableOption "authentik platform agent";
+
+ package = mkOption {
+ type = types.package;
+ default = pkgs.authentik-agent or (throw "authentik-agent package not found. Add the authentik overlay to your nixpkgs.");
+ defaultText = literalExpression "pkgs.authentik-agent";
+ description = "The authentik agent package to use (includes all binaries).";
+ };
+
+ domain = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ description = "The authentik server domain to connect to.";
+ };
+
+ configFile = mkOption {
+ type = types.nullOr types.path;
+ default = null;
+ description = "Path to the authentik configuration file.";
+ };
+
+ extraArgs = mkOption {
+ type = types.listOf types.str;
+ default = [ ];
+ description = "Extra command-line arguments to pass to ak-sysd.";
+ };
+ };
+
+ config = mkIf cfg.enable {
+ # Install the package (makes binaries available in PATH)
+ environment.systemPackages = [ cfg.package ];
+
+ # Expose the .app bundle at the upstream path and install browser native messaging hosts.
+ system.activationScripts.authentik-agent.text = ''
+ echo "Installing authentik Agent.app..."
+ rm -rf "${appPath}"
+ ln -sfn "${packageAppPath}" "${appPath}"
+ mkdir -p /Library/Logs/io.goauthentik
+ chmod 755 /Library/Logs/io.goauthentik
+
+ echo "Preparing authentik runtime directories..."
+ mkdir -p /opt/authentik/config
+ mkdir -p /opt/authentik/domains
+ mkdir -p /opt/authentik/runtime
+ chmod 700 /opt/authentik
+ chmod 700 /opt/authentik/config
+ chmod 700 /opt/authentik/domains
+ chmod 700 /opt/authentik/runtime
+
+ if [ ! -e "${targetConfigPath}" ]; then
+ echo "Seeding default authentik sysd config..."
+ mkdir -p "$(dirname "${targetConfigPath}")"
+ install -m 600 "${defaultSysdConfig}" "${targetConfigPath}"
+ fi
+
+ echo "Installing browser native messaging hosts..."
+ # Chrome/Chromium (system-wide)
+ mkdir -p "/Library/Google/Chrome/NativeMessagingHosts"
+ sed "s|/Applications/authentik Agent.app/Contents/MacOS/ak-browser-support|${browserSupportPath}|g" \
+ "${packageResourcesPath}/browser-host-chrome.json" \
+ > "/Library/Google/Chrome/NativeMessagingHosts/io.goauthentik.platform.json"
+ chmod 644 "/Library/Google/Chrome/NativeMessagingHosts/io.goauthentik.platform.json"
+
+ mkdir -p "/Library/Application Support/Chromium/NativeMessagingHosts"
+ sed "s|/Applications/authentik Agent.app/Contents/MacOS/ak-browser-support|${browserSupportPath}|g" \
+ "${packageResourcesPath}/browser-host-chrome.json" \
+ > "/Library/Application Support/Chromium/NativeMessagingHosts/io.goauthentik.platform.json"
+ chmod 644 "/Library/Application Support/Chromium/NativeMessagingHosts/io.goauthentik.platform.json"
+
+ # Edge (system-wide)
+ mkdir -p "/Library/Microsoft/Edge/NativeMessagingHosts"
+ sed "s|/Applications/authentik Agent.app/Contents/MacOS/ak-browser-support|${browserSupportPath}|g" \
+ "${packageResourcesPath}/browser-host-chrome.json" \
+ > "/Library/Microsoft/Edge/NativeMessagingHosts/io.goauthentik.platform.json"
+ chmod 644 "/Library/Microsoft/Edge/NativeMessagingHosts/io.goauthentik.platform.json"
+
+ # Firefox (system-wide)
+ mkdir -p "/Library/Application Support/Mozilla/NativeMessagingHosts"
+ sed "s|/Applications/authentik Agent.app/Contents/MacOS/ak-browser-support|${browserSupportPath}|g" \
+ "${packageResourcesPath}/browser-host-firefox.json" \
+ > "/Library/Application Support/Mozilla/NativeMessagingHosts/io.goauthentik.platform.json"
+ chmod 644 "/Library/Application Support/Mozilla/NativeMessagingHosts/io.goauthentik.platform.json"
+ '';
+
+ # Launchd daemon for ak-sysd (runs as root)
+ launchd.daemons.io-goauthentik-platform-sysd = {
+ serviceConfig = {
+ Label = "io.goauthentik.platform.sysd";
+ ProgramArguments = [
+ "${packageAppPath}/Contents/MacOS/ak-sysd"
+ "agent"
+ ] ++ lib.optionals (cfg.configFile != null) [
+ "--config-file"
+ (toString cfg.configFile)
+ ] ++ cfg.extraArgs;
+ EnvironmentVariables = mkIf (cfg.domain != null) {
+ AUTHENTIK_DOMAIN = cfg.domain;
+ };
+ UserName = "root";
+ RunAtLoad = true;
+ KeepAlive = true;
+ StandardOutPath = "/Library/Logs/io.goauthentik/sysd.log";
+ StandardErrorPath = "/Library/Logs/io.goauthentik/sysd.log";
+ };
+ };
+ };
+}
diff --git a/nix/packages/ak-agent.nix b/nix/packages/ak-agent.nix
new file mode 100644
index 00000000..405001ba
--- /dev/null
+++ b/nix/packages/ak-agent.nix
@@ -0,0 +1,86 @@
+{ lib
+, buildGoModule
+, version
+, vendorHash
+, goLdflags
+, goBuildInputs
+, stdenv
+}:
+
+buildGoModule {
+ pname = "ak-agent";
+ inherit version vendorHash;
+
+ src = ../..;
+
+ subPackages = [
+ "cmd/agent_local"
+ "cmd/agent_system"
+ "cmd/cli"
+ "cmd/browser_support"
+ ];
+
+ nativeBuildInputs = goBuildInputs;
+
+ ldflags = goLdflags;
+
+ postInstall =
+ let
+ # Reference source root - the space in "authentik Agent.app" is handled in the string
+ srcRoot = ../..;
+ in
+ ''
+ # Rename binaries
+ mv $out/bin/agent_local $out/bin/ak-agent
+ mv $out/bin/agent_system $out/bin/ak-sysd
+ mv $out/bin/cli $out/bin/ak
+ mv $out/bin/browser_support $out/bin/ak-browser-support
+ ln -s $out/bin/ak $out/bin/ak-vault
+ '' + lib.optionalString stdenv.isLinux ''
+ # Install user systemd service file from source
+ mkdir -p $out/lib/systemd/user
+ sed "s|/usr/bin/ak-agent|$out/bin/ak-agent|g" \
+ "${srcRoot}/cmd/agent_local/package/linux/etc/systemd/user/ak-agent.service" \
+ > $out/lib/systemd/user/ak-agent.service
+
+ # Install polkit policy
+ mkdir -p $out/share/polkit-1/actions
+ cp "${srcRoot}/cmd/agent_local/package/linux/usr/share/polkit-1/actions/io.goauthentik.platform.policy" \
+ $out/share/polkit-1/actions/
+ '' + lib.optionalString stdenv.isDarwin ''
+ # Create macOS .app bundle
+ APP_DIR="$out/Applications/authentik Agent.app/Contents"
+ mkdir -p "$APP_DIR/MacOS"
+ mkdir -p "$APP_DIR/Resources"
+ mkdir -p "$out/Library/LaunchDaemons"
+
+ # Copy all binaries into the app bundle
+ cp $out/bin/ak-agent "$APP_DIR/MacOS/ak-agent"
+ cp $out/bin/ak-sysd "$APP_DIR/MacOS/ak-sysd"
+ cp $out/bin/ak "$APP_DIR/MacOS/ak"
+ cp $out/bin/ak-browser-support "$APP_DIR/MacOS/ak-browser-support"
+ ln -s "ak" "$APP_DIR/MacOS/ak-vault"
+
+ # Copy resources from source
+ cp "${srcRoot}/cmd/agent_local/package/macos/authentik Agent.app/Contents/Resources/icon.icns" "$APP_DIR/Resources/"
+ cp "${srcRoot}/cmd/agent_local/package/macos/authentik Agent.app/Contents/Resources/browser-host-chrome.json" "$APP_DIR/Resources/"
+ cp "${srcRoot}/cmd/agent_local/package/macos/authentik Agent.app/Contents/Resources/browser-host-firefox.json" "$APP_DIR/Resources/"
+
+ # Use the current upstream Info.plist as-is so bundle metadata stays aligned.
+ cp "${srcRoot}/cmd/agent_local/package/macos/authentik Agent.app/Contents/Info.plist" \
+ "$APP_DIR/Info.plist"
+
+ # Copy launchd plist from source (daemon.plist)
+ cp "${srcRoot}/cmd/agent_local/package/macos/scripts/daemon.plist" \
+ "$out/Library/LaunchDaemons/io.goauthentik.platform.sysd.plist"
+ '';
+
+ meta = with lib; {
+ description = "authentik Local Agent for user authentication";
+ homepage = "https://goauthentik.io";
+ license = licenses.mit;
+ maintainers = [ ];
+ platforms = platforms.linux ++ platforms.darwin;
+ mainProgram = "ak-agent";
+ };
+}
diff --git a/nix/packages/ak-browser-support.nix b/nix/packages/ak-browser-support.nix
new file mode 100644
index 00000000..5b5182dd
--- /dev/null
+++ b/nix/packages/ak-browser-support.nix
@@ -0,0 +1,45 @@
+{ lib
+, buildGoModule
+, version
+, vendorHash
+, goLdflags
+, goBuildInputs
+, stdenv
+}:
+
+buildGoModule {
+ pname = "ak-browser-support";
+ inherit version vendorHash;
+
+ src = ../..;
+
+ subPackages = [ "cmd/browser_support" ];
+
+ nativeBuildInputs = goBuildInputs;
+
+ ldflags = goLdflags;
+
+ postInstall = let
+ srcRoot = ../..;
+ in ''
+ mv $out/bin/browser_support $out/bin/ak-browser-support
+
+ # Include browser native messaging manifests in share for modules to install
+ mkdir -p $out/share/authentik
+ sed "s|/usr/bin/ak-browser-support|$out/bin/ak-browser-support|g" \
+ "${srcRoot}/cmd/agent_system/package/linux/browser-host-chrome.json" \
+ > $out/share/authentik/browser-host-chrome.json
+ sed "s|/usr/bin/ak-browser-support|$out/bin/ak-browser-support|g" \
+ "${srcRoot}/cmd/agent_system/package/linux/browser-host-firefox.json" \
+ > $out/share/authentik/browser-host-firefox.json
+ '';
+
+ meta = with lib; {
+ description = "authentik Browser Support for native messaging";
+ homepage = "https://goauthentik.io";
+ license = licenses.mit;
+ maintainers = [ ];
+ platforms = platforms.linux ++ platforms.darwin;
+ mainProgram = "ak-browser-support";
+ };
+}
diff --git a/nix/packages/ak-cli.nix b/nix/packages/ak-cli.nix
new file mode 100644
index 00000000..c94afa8b
--- /dev/null
+++ b/nix/packages/ak-cli.nix
@@ -0,0 +1,36 @@
+{ lib
+, buildGoModule
+, version
+, vendorHash
+, goLdflags
+, goBuildInputs
+}:
+
+buildGoModule {
+ pname = "ak-cli";
+ inherit version vendorHash;
+
+ src = ../..;
+
+ subPackages = [ "cmd/cli" ];
+
+ nativeBuildInputs = goBuildInputs;
+
+ ldflags = goLdflags;
+
+ postInstall = ''
+ mv $out/bin/cli $out/bin/ak
+
+ # Match the packaged CLI entrypoint aliases.
+ ln -s $out/bin/ak $out/bin/ak-vault
+ '';
+
+ meta = with lib; {
+ description = "authentik Platform CLI";
+ homepage = "https://goauthentik.io";
+ license = licenses.mit;
+ maintainers = [ ];
+ platforms = platforms.linux ++ platforms.darwin;
+ mainProgram = "ak";
+ };
+}
diff --git a/nix/packages/ak-sysd.nix b/nix/packages/ak-sysd.nix
new file mode 100644
index 00000000..23159805
--- /dev/null
+++ b/nix/packages/ak-sysd.nix
@@ -0,0 +1,56 @@
+{ lib
+, buildGoModule
+, stdenv
+, version
+, vendorHash
+, goLdflags
+, goBuildInputs
+}:
+
+buildGoModule {
+ pname = "ak-sysd";
+ inherit version vendorHash;
+
+ src = ../..;
+
+ subPackages = [ "cmd/agent_system" ];
+
+ nativeBuildInputs = goBuildInputs;
+
+ ldflags = goLdflags;
+
+ postInstall = ''
+ mv $out/bin/agent_system $out/bin/ak-sysd
+
+ # Install systemd service file (Linux only)
+ ${lib.optionalString stdenv.isLinux ''
+ mkdir -p $out/lib/systemd/system
+ cat > $out/lib/systemd/system/ak-sysd.service << EOF
+ [Unit]
+ Description=authentik sysd
+ After=network.target
+
+ [Service]
+ Restart=always
+ ExecStart=$out/bin/ak-sysd agent
+ RuntimeDirectory=authentik
+ RuntimeDirectoryMode=0777
+
+ [Install]
+ WantedBy=multi-user.target
+ EOF
+ ''}
+
+ # Create default config directory structure
+ mkdir -p $out/etc/authentik
+ '';
+
+ meta = with lib; {
+ description = "authentik System Agent daemon";
+ homepage = "https://goauthentik.io";
+ license = licenses.mit;
+ maintainers = [ ];
+ platforms = platforms.linux ++ platforms.darwin;
+ mainProgram = "ak-sysd";
+ };
+}
diff --git a/nix/packages/libnss-authentik.nix b/nix/packages/libnss-authentik.nix
new file mode 100644
index 00000000..c85feffb
--- /dev/null
+++ b/nix/packages/libnss-authentik.nix
@@ -0,0 +1,44 @@
+{ lib
+, stdenv
+, rustPlatform
+, pkg-config
+, openssl
+, version
+}:
+
+rustPlatform.buildRustPackage {
+ pname = "libnss-authentik";
+ inherit version;
+
+ src = ../..;
+
+ cargoLock = {
+ lockFile = ../../Cargo.lock;
+ };
+
+ # Build only the NSS module
+ buildAndTestSubdir = "nss";
+
+ nativeBuildInputs = [
+ pkg-config
+ ];
+
+ buildInputs = [
+ openssl
+ ];
+
+ postInstall = ''
+ # Install the NSS module (must be named libnss_.so.2)
+ mkdir -p $out/lib
+ cp target/${stdenv.hostPlatform.rust.rustcTarget}/release/libauthentik_nss.so $out/lib/libnss_authentik.so.2
+ ln -s libnss_authentik.so.2 $out/lib/libnss_authentik.so
+ '';
+
+ meta = with lib; {
+ description = "NSS module for authentik name resolution";
+ homepage = "https://goauthentik.io";
+ license = licenses.mit;
+ maintainers = [ ];
+ platforms = platforms.linux;
+ };
+}
diff --git a/nix/packages/libpam-authentik.nix b/nix/packages/libpam-authentik.nix
new file mode 100644
index 00000000..bac78273
--- /dev/null
+++ b/nix/packages/libpam-authentik.nix
@@ -0,0 +1,61 @@
+{ lib
+, stdenv
+, rustPlatform
+, pkg-config
+, pam
+, openssl
+, version
+}:
+
+rustPlatform.buildRustPackage {
+ pname = "libpam-authentik";
+ inherit version;
+
+ src = ../..;
+
+ cargoLock = {
+ lockFile = ../../Cargo.lock;
+ };
+
+ # Build only the PAM module
+ buildAndTestSubdir = "pam";
+
+ nativeBuildInputs = [
+ pkg-config
+ ];
+
+ buildInputs = [
+ pam
+ openssl
+ ];
+
+ postInstall = ''
+ # Install the PAM module to the correct location
+ mkdir -p $out/lib/security
+ cp target/${stdenv.hostPlatform.rust.rustcTarget}/release/libauthentik_pam.so $out/lib/security/pam_authentik.so
+
+ # Install PAM configuration for pam-auth-update
+ mkdir -p $out/share/pam-configs
+ cat > $out/share/pam-configs/authentik << 'EOF'
+ Name: authentik Authentication
+ Default: yes
+ Priority: 512
+ Auth-Type: Primary
+ Auth:
+ [success=end default=ignore] pam_authentik.so
+ Auth-Initial:
+ [success=end default=ignore] pam_authentik.so
+ Session-Type: Additional
+ Session:
+ required pam_authentik.so
+ EOF
+ '';
+
+ meta = with lib; {
+ description = "PAM module for authentik authentication";
+ homepage = "https://goauthentik.io";
+ license = licenses.mit;
+ maintainers = [ ];
+ platforms = platforms.linux;
+ };
+}
diff --git a/pkg/agent_system/agent_starter/starter.go b/pkg/agent_system/agent_starter/starter.go
index 57083fee..c0bf7389 100644
--- a/pkg/agent_system/agent_starter/starter.go
+++ b/pkg/agent_system/agent_starter/starter.go
@@ -3,6 +3,8 @@ package agentstarter
import (
"errors"
"os"
+ "path/filepath"
+ "runtime"
"time"
"github.com/avast/retry-go/v4"
@@ -10,7 +12,6 @@ import (
"goauthentik.io/platform/pkg/agent_system/component"
"goauthentik.io/platform/pkg/agent_system/config"
"goauthentik.io/platform/pkg/agent_system/session"
- "goauthentik.io/platform/pkg/platform/pstr"
"goauthentik.io/platform/pkg/shared/events"
"goauthentik.io/platform/vnd/fleet/orbit/pkg/execuser"
userpkg "goauthentik.io/platform/vnd/fleet/orbit/pkg/user"
@@ -50,7 +51,12 @@ func (as *Server) Stop() error {
func (as *Server) RegisterForID(id string, s grpc.ServiceRegistrar) {}
func (as *Server) start() {
- if _, err := os.Stat(as.agentExec().ForCurrent()); errors.Is(err, os.ErrNotExist) {
+ agentExec := as.agentExec()
+ if agentExec == "" {
+ return
+ }
+ if _, err := os.Stat(agentExec); errors.Is(err, os.ErrNotExist) {
+ as.log.WithField("path", agentExec).Debug("agent executable path not found, skipping")
return
}
for {
@@ -75,11 +81,16 @@ func (as *Server) start() {
}
}
-func (as *Server) agentExec() pstr.PlatformString {
- return pstr.PlatformString{
- Darwin: new("/Applications/authentik Agent.app"),
- Linux: new("/usr/bin/ak-agent"),
- Windows: new(`C:\Program Files\Authentik Security Inc\agent\ak-agent.exe`),
+func (as *Server) agentExec() string {
+ switch runtime.GOOS {
+ case "darwin":
+ return darwinAgentExec()
+ case "linux":
+ return "/usr/bin/ak-agent"
+ case "windows":
+ return `C:\Program Files\Authentik Security Inc\agent\ak-agent.exe`
+ default:
+ return ""
}
}
@@ -113,10 +124,63 @@ func (as *Server) startSingle() error {
// To be able to run the desktop application (mostly to register the icon in the system tray)
// we need to run the application as the login user.
// Package execuser provides multi-platform support for this.
- lastLogs, err := execuser.Run(as.agentExec().ForCurrent(), opts...)
+ lastLogs, err := execuser.Run(as.agentExec(), opts...)
if err != nil {
as.log.WithField("logs", lastLogs).WithError(err).Debug("execuser.Run")
return err
}
return nil
}
+
+func darwinAgentExec() string {
+ if override := os.Getenv("AUTHENTIK_AGENT_APP_PATH"); override != "" {
+ return override
+ }
+ executablePath, err := os.Executable()
+ if err != nil {
+ executablePath = ""
+ }
+ return firstExistingPath(darwinAgentExecCandidates(executablePath), func(path string) bool {
+ _, err := os.Stat(path)
+ return err == nil
+ })
+}
+
+func darwinAgentExecCandidates(executablePath string) []string {
+ candidates := []string{
+ "/Applications/authentik Agent.app",
+ "/Applications/Nix Apps/authentik Agent.app",
+ }
+ if executablePath != "" {
+ candidates = append(candidates, filepath.Clean(filepath.Join(executablePath, "..", "..", "..")))
+ }
+ return uniqueStrings(candidates)
+}
+
+func firstExistingPath(candidates []string, exists func(string) bool) string {
+ for _, candidate := range candidates {
+ if candidate != "" && exists(candidate) {
+ return candidate
+ }
+ }
+ if len(candidates) == 0 {
+ return ""
+ }
+ return candidates[0]
+}
+
+func uniqueStrings(values []string) []string {
+ seen := map[string]struct{}{}
+ unique := make([]string, 0, len(values))
+ for _, value := range values {
+ if value == "" {
+ continue
+ }
+ if _, ok := seen[value]; ok {
+ continue
+ }
+ seen[value] = struct{}{}
+ unique = append(unique, value)
+ }
+ return unique
+}
diff --git a/pkg/agent_system/agent_starter/starter_test.go b/pkg/agent_system/agent_starter/starter_test.go
new file mode 100644
index 00000000..7204edb0
--- /dev/null
+++ b/pkg/agent_system/agent_starter/starter_test.go
@@ -0,0 +1,52 @@
+package agentstarter
+
+import "testing"
+
+func TestDarwinAgentExecCandidates(t *testing.T) {
+ executablePath := "/nix/store/test-ak-agent/Applications/authentik Agent.app/Contents/MacOS/ak-sysd"
+
+ candidates := darwinAgentExecCandidates(executablePath)
+
+ if len(candidates) != 3 {
+ t.Fatalf("expected 3 candidates, got %d: %#v", len(candidates), candidates)
+ }
+ if candidates[0] != "/Applications/authentik Agent.app" {
+ t.Fatalf("unexpected first candidate: %q", candidates[0])
+ }
+ if candidates[1] != "/Applications/Nix Apps/authentik Agent.app" {
+ t.Fatalf("unexpected second candidate: %q", candidates[1])
+ }
+ if candidates[2] != "/nix/store/test-ak-agent/Applications/authentik Agent.app" {
+ t.Fatalf("unexpected bundle candidate: %q", candidates[2])
+ }
+}
+
+func TestFirstExistingPath(t *testing.T) {
+ candidates := []string{
+ "/Applications/authentik Agent.app",
+ "/Applications/Nix Apps/authentik Agent.app",
+ }
+
+ selected := firstExistingPath(candidates, func(path string) bool {
+ return path == "/Applications/Nix Apps/authentik Agent.app"
+ })
+
+ if selected != "/Applications/Nix Apps/authentik Agent.app" {
+ t.Fatalf("unexpected selected path: %q", selected)
+ }
+}
+
+func TestFirstExistingPathFallback(t *testing.T) {
+ candidates := []string{
+ "/Applications/authentik Agent.app",
+ "/Applications/Nix Apps/authentik Agent.app",
+ }
+
+ selected := firstExistingPath(candidates, func(string) bool {
+ return false
+ })
+
+ if selected != "/Applications/authentik Agent.app" {
+ t.Fatalf("unexpected fallback path: %q", selected)
+ }
+}
diff --git a/pkg/agent_system/config/config.go b/pkg/agent_system/config/config.go
index 23e904c9..7b2cadac 100644
--- a/pkg/agent_system/config/config.go
+++ b/pkg/agent_system/config/config.go
@@ -78,12 +78,12 @@ func (c *Config) Domains() []*DomainConfig {
func (c *Config) SaveDomain(dom *DomainConfig) error {
path := filepath.Join(c.DomainDir, dom.Domain+".json")
+ dom.FallbackToken = dom.Token
err := keyring.Set(keyring.Service("domain_token"), dom.Domain, keyring.AccessibleAlways, dom.Token)
if err != nil {
if !errors.Is(err, keyring.ErrUnsupportedPlatform) {
c.log.WithError(err).Warning("failed to save domain token in keyring")
}
- dom.FallbackToken = dom.Token
}
b, err := json.Marshal(dom)
if err != nil {
@@ -103,7 +103,6 @@ func (c *Config) DeleteDomain(dom *DomainConfig) error {
if !errors.Is(err, keyring.ErrUnsupportedPlatform) {
c.log.WithError(err).Warning("failed to delete domain token in keyring")
}
- dom.FallbackToken = dom.Token
}
err = os.Remove(path)
if err != nil {
diff --git a/pkg/agent_system/config/config_test.go b/pkg/agent_system/config/config_test.go
new file mode 100644
index 00000000..c8ae6125
--- /dev/null
+++ b/pkg/agent_system/config/config_test.go
@@ -0,0 +1,41 @@
+package config
+
+import (
+ "encoding/json"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+ "goauthentik.io/platform/pkg/storage/state"
+)
+
+func TestSaveDomainPersistsFallbackToken(t *testing.T) {
+ dir := t.TempDir()
+ testState, err := state.Open(filepath.Join(dir, "state.db"), nil)
+ require.NoError(t, err)
+ st = testState
+ t.Cleanup(func() {
+ require.NoError(t, testState.Close())
+ st = nil
+ })
+
+ cfg := &Config{
+ DomainDir: dir,
+ log: (&Config{}).Default().(*Config).log,
+ }
+
+ dom := cfg.NewDomain()
+ dom.Domain = "authentik"
+ dom.AuthentikURL = "https://authentik.company"
+ dom.Token = "device-token"
+
+ require.NoError(t, cfg.SaveDomain(dom))
+
+ raw, err := os.ReadFile(filepath.Join(dir, "authentik.json"))
+ require.NoError(t, err)
+
+ saved := &DomainConfig{}
+ require.NoError(t, json.Unmarshal(raw, saved))
+ require.Equal(t, "device-token", saved.FallbackToken)
+}
diff --git a/pkg/agent_system/device/signed.go b/pkg/agent_system/device/signed.go
index c41f0360..8a950b9d 100644
--- a/pkg/agent_system/device/signed.go
+++ b/pkg/agent_system/device/signed.go
@@ -6,7 +6,6 @@ import (
"time"
"github.com/MicahParks/jwkset"
- "github.com/MicahParks/keyfunc/v3"
"github.com/golang-jwt/jwt/v5"
"github.com/mitchellh/mapstructure"
"goauthentik.io/platform/pkg/agent_system/config"
@@ -16,28 +15,48 @@ import (
"goauthentik.io/platform/pkg/platform/facts/hardware"
)
-func (ds *Server) validateChallenge(ctx context.Context, rawToken string) (*token.Token, *config.DomainConfig, error) {
- for _, dom := range config.Manager().Get().Domains() {
- var st jwkset.Storage
- jw := jwkset.JWKSMarshal{}
- err := mapstructure.Decode(dom.Config().JwksChallenge, &jw)
- if err != nil {
- ds.log.WithField("domain", dom.Domain).WithError(err).Warning("failed to load config")
- continue
+func parseChallengeToken(rawToken string, rawJWKS map[string]any) (*jwt.Token, error) {
+ jw := jwkset.JWKSMarshal{}
+ err := mapstructure.Decode(rawJWKS, &jw)
+ if err != nil {
+ return nil, err
+ }
+ keys, err := jw.JWKSlice()
+ if err != nil {
+ return nil, err
+ }
+ headerOnly := &token.AuthentikClaims{}
+ parser := jwt.NewParser()
+ unverified, _, err := parser.ParseUnverified(rawToken, headerOnly)
+ if err != nil {
+ return nil, err
+ }
+ targetKID, _ := unverified.Header["kid"].(string)
+ var lastErr error
+ for _, key := range keys {
+ if targetKID != "" {
+ marshaled := key.Marshal()
+ if marshaled.KID != "" && marshaled.KID != targetKID {
+ continue
+ }
}
- sst, err := jw.ToStorage()
- if err != nil {
- ds.log.WithField("domain", dom.Domain).WithError(err).Warning("failed to parse jwks")
- continue
+ parsed, err := jwt.ParseWithClaims(rawToken, &token.AuthentikClaims{}, func(*jwt.Token) (any, error) {
+ return key.Key(), nil
+ })
+ if err == nil {
+ return parsed, nil
}
- st = sst
+ lastErr = err
+ }
+ if lastErr != nil {
+ return nil, lastErr
+ }
+ return nil, errors.New("no matching jwk found for challenge")
+}
- k, err := keyfunc.New(keyfunc.Options{Storage: st, Ctx: ctx})
- if err != nil {
- ds.log.WithField("domain", dom.Domain).WithError(err).Warning("failed to create keyfunc")
- continue
- }
- t, err := jwt.ParseWithClaims(rawToken, &token.AuthentikClaims{}, k.Keyfunc)
+func (ds *Server) validateChallenge(_ context.Context, rawToken string) (*token.Token, *config.DomainConfig, error) {
+ for _, dom := range config.Manager().Get().Domains() {
+ t, err := parseChallengeToken(rawToken, dom.Config().JwksChallenge)
if err != nil {
ds.log.WithField("domain", dom.Domain).WithError(err).Warning("failed to validate token")
continue
diff --git a/pkg/agent_system/device/signed_test.go b/pkg/agent_system/device/signed_test.go
new file mode 100644
index 00000000..ed90b796
--- /dev/null
+++ b/pkg/agent_system/device/signed_test.go
@@ -0,0 +1,53 @@
+package device
+
+import (
+ "crypto/rand"
+ "crypto/rsa"
+ "encoding/json"
+ "testing"
+
+ "github.com/MicahParks/jwkset"
+ "github.com/golang-jwt/jwt/v5"
+ "github.com/stretchr/testify/require"
+)
+
+func TestParseChallengeToken(t *testing.T) {
+ key, err := rsa.GenerateKey(rand.Reader, 2048)
+ require.NoError(t, err)
+
+ jwk, err := jwkset.NewJWKFromKey(&key.PublicKey, jwkset.JWKOptions{
+ Metadata: jwkset.JWKMetadataOptions{
+ ALG: jwkset.ALG("RS256"),
+ KID: "test-kid",
+ USE: jwkset.USE("sig"),
+ },
+ })
+ require.NoError(t, err)
+
+ jwksBytes, err := json.Marshal(jwkset.JWKSMarshal{
+ Keys: []jwkset.JWKMarshal{jwk.Marshal()},
+ })
+ require.NoError(t, err)
+
+ var jwks map[string]any
+ require.NoError(t, json.Unmarshal(jwksBytes, &jwks))
+
+ rawToken, err := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{
+ "iss": "stage",
+ "aud": "goauthentik.io/platform/endpoint",
+ }).SignedString(key)
+ require.NoError(t, err)
+
+ // Rebuild the token with the correct kid header.
+ signed := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{
+ "iss": "stage",
+ "aud": "goauthentik.io/platform/endpoint",
+ })
+ signed.Header["kid"] = "test-kid"
+ rawToken, err = signed.SignedString(key)
+ require.NoError(t, err)
+
+ parsed, err := parseChallengeToken(rawToken, jwks)
+ require.NoError(t, err)
+ require.NotNil(t, parsed)
+}
diff --git a/pkg/browser_support/handler.go b/pkg/browser_support/handler.go
index 439e8cd8..38f24f1b 100644
--- a/pkg/browser_support/handler.go
+++ b/pkg/browser_support/handler.go
@@ -31,6 +31,7 @@ func (m message) MessageID() string {
type response struct {
Data map[string]any `json:"data"`
+ Error string `json:"error,omitempty"`
ResponseTo string `json:"response_to"`
}
@@ -74,7 +75,6 @@ func (bs *BrowserSupport) setup() {
}, nil
})
bs.l.Handle("get_token", func(in message) (*response, error) {
- bs.log.Debugf("Browser host message: '%+v'\n", in)
curr, err := bs.agentClient.GetCurrentToken(bs.ctx, &pb.CurrentTokenRequest{
Header: &pb.RequestHeader{
Profile: in.Profile,
@@ -83,7 +83,9 @@ func (bs *BrowserSupport) setup() {
})
if err != nil {
bs.log.WithError(err).Warning("failed to get current token")
- return nil, err
+ return &response{
+ Error: err.Error(),
+ }, nil
}
return &response{
Data: map[string]any{
@@ -96,7 +98,9 @@ func (bs *BrowserSupport) setup() {
res, err := bs.agentClient.ListProfiles(bs.ctx, &emptypb.Empty{})
if err != nil {
bs.log.WithError(err).Warning("failed to list profiles")
- return nil, err
+ return &response{
+ Error: err.Error(),
+ }, nil
}
return &response{
Data: map[string]any{
@@ -113,7 +117,9 @@ func (bs *BrowserSupport) setup() {
})
if err != nil {
bs.log.WithError(err).Warning("failed to get endpoint header")
- return nil, err
+ return &response{
+ Error: err.Error(),
+ }, nil
}
return &response{
Data: map[string]any{