From 396632ed01d950cc461306a0dbfee148637d853e Mon Sep 17 00:00:00 2001 From: Dominic R Date: Tue, 30 Dec 2025 16:09:07 -0500 Subject: [PATCH 01/14] packaging: add nix --- .github/actions/apt/action.yml | 13 +++ .github/actions/nix/action.yml | 161 ++++++++++++++++++++++++++++ .github/actions/repo/index.html | 61 +++++++++++ .github/workflows/build.yml | 77 ++++++++++++- .gitignore | 3 + Makefile | 71 +++++++++++- flake.lock | 82 ++++++++++++++ flake.nix | 150 ++++++++++++++++++++++++++ nix/modules/authentik.nix | 155 ++++++++++++++++++++++++++ nix/modules/darwin.nix | 76 +++++++++++++ nix/packages/ak-agent.nix | 83 ++++++++++++++ nix/packages/ak-browser-support.nix | 45 ++++++++ nix/packages/ak-cli.nix | 37 +++++++ nix/packages/ak-sysd.nix | 56 ++++++++++ nix/packages/libnss-authentik.nix | 44 ++++++++ nix/packages/libpam-authentik.nix | 61 +++++++++++ 16 files changed, 1171 insertions(+), 4 deletions(-) create mode 100644 .github/actions/nix/action.yml create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 nix/modules/authentik.nix create mode 100644 nix/modules/darwin.nix create mode 100644 nix/packages/ak-agent.nix create mode 100644 nix/packages/ak-browser-support.nix create mode 100644 nix/packages/ak-cli.nix create mode 100644 nix/packages/ak-sysd.nix create mode 100644 nix/packages/libnss-authentik.nix create mode 100644 nix/packages/libpam-authentik.nix 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 @@

Step 2: Install

+

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

+ + + + + + + + +
PackageDescriptionPlatforms
ak-cliCommand-line interfaceLinux, macOS
ak-sysdSystem daemonLinux, macOS
ak-agentLocal agent (includes all binaries on macOS)Linux, macOS
ak-browser-supportBrowser extension supportLinux, macOS
libpam-authentikPAM authentication moduleLinux
libnss-authentikNSS name resolution moduleLinux
+
+
+

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..3055ab91 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,14 @@ jobs: with: name: all-packages path: ./deploy/packages + - name: Show deployment structure + shell: bash + run: | + echo "Deployment structure:" + find ./deploy -type f | head -50 + echo "---" + 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..158d18f6 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 @@ -151,6 +155,7 @@ ee/psso/%: ee/wcp/%: "$(MAKE)" -C "${TOP}/ee/wcp/" $* +<<<<<<< HEAD containers/selenium/%: "$(MAKE)" -C "${TOP}/containers/selenium" $* @@ -159,3 +164,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/flake.lock b/flake.lock new file mode 100644 index 00000000..468e761b --- /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": 1766714959, + "narHash": "sha256-+2xdB27fVMEVKWmh7UtpBGgK1FOVdk5ttV3ZPhpdqVY=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "8970f698c39f490df47e4cbb7beae7869ddf1978", + "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": 1766717007, + "narHash": "sha256-ZjLiHCHgoH2maP5ZAKn0anrHymbjGOS5/PZqfJUK8Ik=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "a18efe8a9112175e43397cf870fb6bc1ca480548", + "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..45051312 --- /dev/null +++ b/flake.nix @@ -0,0 +1,150 @@ +{ + description = "authentik platform"; + + nixConfig = { + extra-substituters = [ "https://authentik-pkg.netlify.app/nix" ]; + trusted-substituters = [ "https://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 }: + flake-utils.lib.eachSystem [ + "x86_64-linux" + "aarch64-linux" + "x86_64-darwin" + "aarch64-darwin" + ] (system: + let + overlays = [ rust-overlay.overlays.default ]; + pkgs = import nixpkgs { + inherit system overlays; + }; + + version = "0.35.3"; + vendorHash = "sha256-qoObvQs5Pk7CYci6PNRzzt797v94ZLTEZpTRRV6QKZM="; + + # Common Go build settings + goBuildInputs = with pkgs; [ + pkg-config + ] ++ lib.optionals stdenv.isDarwin [ + libiconv + ]; + + goLdflags = [ + "-s" "-w" + "-X goauthentik.io/platform/pkg/meta.Version=${version}" + "-X goauthentik.io/platform/pkg/meta.BuildHash=nix-${self.shortRev or "dirty"}" + ]; + + # Rust toolchain + rustToolchain = pkgs.rust-bin.stable.latest.default.override { + extensions = [ "rust-src" ]; + }; + + # Platform checks + isLinux = pkgs.stdenv.isLinux; + isDarwin = pkgs.stdenv.isDarwin; + + in { + packages = { + ak-cli = pkgs.callPackage ./nix/packages/ak-cli.nix { + inherit version vendorHash goLdflags goBuildInputs; + }; + + ak-sysd = pkgs.callPackage ./nix/packages/ak-sysd.nix { + inherit version vendorHash goLdflags goBuildInputs; + }; + + ak-agent = pkgs.callPackage ./nix/packages/ak-agent.nix { + inherit version vendorHash goLdflags goBuildInputs; + }; + + ak-browser-support = pkgs.callPackage ./nix/packages/ak-browser-support.nix { + inherit version vendorHash goLdflags goBuildInputs; + }; + + # Rust packages only on Linux + libpam-authentik = if isLinux then + pkgs.callPackage ./nix/packages/libpam-authentik.nix { + inherit version; + } + else + pkgs.runCommand "libpam-authentik-unsupported" {} '' + echo "libpam-authentik is only available on Linux" >&2 + exit 1 + ''; + + libnss-authentik = if isLinux then + pkgs.callPackage ./nix/packages/libnss-authentik.nix { + inherit version; + } + else + pkgs.runCommand "libnss-authentik-unsupported" {} '' + echo "libnss-authentik is only available on Linux" >&2 + exit 1 + ''; + + default = self.packages.${system}.ak-agent; + }; + + devShells.default = pkgs.mkShell { + buildInputs = with pkgs; [ + go + gopls + gotools + go-tools + rustToolchain + rust-analyzer + protobuf + protoc-gen-go + protoc-gen-go-grpc + pkg-config + openssl + nodejs + gnumake + ] ++ lib.optionals isLinux [ + pam + ] ++ lib.optionals isDarwin [ + libiconv + ]; + + shellHook = '' + echo "authentik platform development shell" + echo "Go: $(go version)" + echo "Rust: $(rustc --version)" + ''; + }; + + # Expose individual package checks + checks = { + ak-cli = self.packages.${system}.ak-cli; + }; + } + ) // { + # NixOS module + nixosModules.default = import ./nix/modules/authentik.nix; + nixosModules.authentik = self.nixosModules.default; + + # nix-darwin module + darwinModules.default = import ./nix/modules/darwin.nix; + darwinModules.authentik = self.darwinModules.default; + + # Overlay for use in other flakes + overlays.default = final: prev: { + authentik-cli = self.packages.${prev.system}.ak-cli; + authentik-sysd = self.packages.${prev.system}.ak-sysd; + authentik-agent = self.packages.${prev.system}.ak-agent; + authentik-pam = self.packages.${prev.system}.libpam-authentik; + authentik-nss = self.packages.${prev.system}.libnss-authentik; + }; + }; +} 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..582a9f09 --- /dev/null +++ b/nix/modules/darwin.nix @@ -0,0 +1,76 @@ +{ 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-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."; + }; + + 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 ]; + + # Copy .app bundle to /Applications and install browser native messaging hosts + system.activationScripts.postActivation.text = '' + echo "Installing authentik Agent.app..." + rm -rf "/Applications/authentik Agent.app" + cp -r "${cfg.package}/Applications/authentik Agent.app" "/Applications/" + chmod -R 755 "/Applications/authentik Agent.app" + mkdir -p /Library/Logs/io.goauthentik + + echo "Installing browser native messaging hosts..." + # Chrome/Chromium (system-wide) + mkdir -p "/Library/Google/Chrome/NativeMessagingHosts" + cp "/Applications/authentik Agent.app/Contents/Resources/browser-host-chrome.json" \ + "/Library/Google/Chrome/NativeMessagingHosts/io.goauthentik.platform.json" + + # Edge (system-wide) + mkdir -p "/Library/Microsoft/Edge/NativeMessagingHosts" + cp "/Applications/authentik Agent.app/Contents/Resources/browser-host-chrome.json" \ + "/Library/Microsoft/Edge/NativeMessagingHosts/io.goauthentik.platform.json" + + # Firefox (system-wide) + mkdir -p "/Library/Application Support/Mozilla/NativeMessagingHosts" + cp "/Applications/authentik Agent.app/Contents/Resources/browser-host-firefox.json" \ + "/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 = [ + "/Applications/authentik Agent.app/Contents/MacOS/ak-sysd" + "agent" + ] ++ cfg.extraArgs; + 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..31f7c853 --- /dev/null +++ b/nix/packages/ak-agent.nix @@ -0,0 +1,83 @@ +{ 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 + '' + 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" + + # 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/" + + # Copy Info.plist from source and substitute version + sed "s|0.35.2|${version}|g" \ + "${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..ea87ad9f --- /dev/null +++ b/nix/packages/ak-cli.nix @@ -0,0 +1,37 @@ +{ 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 + + # Create symlinks for subcommands + ln -s $out/bin/ak $out/bin/ak-vault + ln -s $out/bin/ak $out/bin/ak-browser-support + ''; + + 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; + }; +} From 654a9ddeab6d98329a51202cad56b770c4ec9792 Mon Sep 17 00:00:00 2001 From: Dominic R Date: Tue, 30 Dec 2025 16:13:01 -0500 Subject: [PATCH 02/14] packaging/nix: Bump to 0.35.4 --- flake.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index 45051312..8de65781 100644 --- a/flake.nix +++ b/flake.nix @@ -29,7 +29,7 @@ inherit system overlays; }; - version = "0.35.3"; + version = "0.35.4"; vendorHash = "sha256-qoObvQs5Pk7CYci6PNRzzt797v94ZLTEZpTRRV6QKZM="; # Common Go build settings From 7808970736e9c30ea4bf958f2baf33b35ef8faee Mon Sep 17 00:00:00 2001 From: Dominic R Date: Tue, 30 Dec 2025 17:41:50 -0500 Subject: [PATCH 03/14] wip --- .github/workflows/build.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3055ab91..ad554182 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -255,9 +255,6 @@ jobs: - name: Show deployment structure shell: bash run: | - echo "Deployment structure:" - find ./deploy -type f | head -50 - echo "---" 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 From 878e79a35fec740d2f3299f135861058ffbcdf1d Mon Sep 17 00:00:00 2001 From: Dominic R Date: Sat, 7 Mar 2026 20:19:50 -0500 Subject: [PATCH 04/14] packaging/nix: rebase on main and clean flake --- flake.lock | 12 +-- flake.nix | 222 ++++++++++++++++++-------------------- nix/modules/darwin.nix | 21 +++- nix/packages/ak-agent.nix | 87 +++++++-------- nix/packages/ak-cli.nix | 3 +- 5 files changed, 171 insertions(+), 174 deletions(-) diff --git a/flake.lock b/flake.lock index 468e761b..56c7a564 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1766714959, - "narHash": "sha256-+2xdB27fVMEVKWmh7UtpBGgK1FOVdk5ttV3ZPhpdqVY=", + "lastModified": 1772736753, + "narHash": "sha256-au/m3+EuBLoSzWUCb64a/MZq6QUtOV8oC0D9tY2scPQ=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "8970f698c39f490df47e4cbb7beae7869ddf1978", + "rev": "917fec990948658ef1ccd07cef2a1ef060786846", "type": "github" }, "original": { @@ -48,11 +48,11 @@ ] }, "locked": { - "lastModified": 1766717007, - "narHash": "sha256-ZjLiHCHgoH2maP5ZAKn0anrHymbjGOS5/PZqfJUK8Ik=", + "lastModified": 1772852295, + "narHash": "sha256-3FB/WzLZSiU2Mc50C9q9VXU1LRUZbsU6UHKmZG1C+hU=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "a18efe8a9112175e43397cf870fb6bc1ca480548", + "rev": "c10801f59c68e14c308aea8fa6b0b3d81d43c61e", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 8de65781..51ef9dfa 100644 --- a/flake.nix +++ b/flake.nix @@ -1,12 +1,6 @@ { description = "authentik platform"; - nixConfig = { - extra-substituters = [ "https://authentik-pkg.netlify.app/nix" ]; - trusted-substituters = [ "https://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"; @@ -17,134 +11,122 @@ }; outputs = { self, nixpkgs, flake-utils, rust-overlay }: - flake-utils.lib.eachSystem [ - "x86_64-linux" - "aarch64-linux" - "x86_64-darwin" - "aarch64-darwin" - ] (system: - let - overlays = [ rust-overlay.overlays.default ]; - pkgs = import nixpkgs { - inherit system overlays; - }; - - version = "0.35.4"; - vendorHash = "sha256-qoObvQs5Pk7CYci6PNRzzt797v94ZLTEZpTRRV6QKZM="; - - # Common Go build settings - goBuildInputs = with pkgs; [ - pkg-config - ] ++ lib.optionals stdenv.isDarwin [ - libiconv - ]; - - goLdflags = [ - "-s" "-w" - "-X goauthentik.io/platform/pkg/meta.Version=${version}" - "-X goauthentik.io/platform/pkg/meta.BuildHash=nix-${self.shortRev or "dirty"}" - ]; - - # Rust toolchain - rustToolchain = pkgs.rust-bin.stable.latest.default.override { - extensions = [ "rust-src" ]; + 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; }; - - # Platform checks - isLinux = pkgs.stdenv.isLinux; - isDarwin = pkgs.stdenv.isDarwin; - - in { - packages = { - ak-cli = pkgs.callPackage ./nix/packages/ak-cli.nix { - inherit version vendorHash goLdflags goBuildInputs; + in + assert version == cargoVersion; + flake-utils.lib.eachSystem systems + (system: + let + pkgs = import nixpkgs { + inherit system; + overlays = [ rust-overlay.overlays.default ]; }; - - ak-sysd = pkgs.callPackage ./nix/packages/ak-sysd.nix { - inherit version vendorHash goLdflags goBuildInputs; + lib = pkgs.lib; + rustToolchain = pkgs.rust-bin.stable.latest.default.override { + extensions = [ "rust-src" ]; }; - - ak-agent = pkgs.callPackage ./nix/packages/ak-agent.nix { - inherit version vendorHash goLdflags goBuildInputs; + 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}" + ]; }; - - ak-browser-support = pkgs.callPackage ./nix/packages/ak-browser-support.nix { - inherit version vendorHash goLdflags goBuildInputs; + 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; }; - - # Rust packages only on Linux - libpam-authentik = if isLinux then - pkgs.callPackage ./nix/packages/libpam-authentik.nix { + linuxPackages = lib.optionalAttrs pkgs.stdenv.isLinux { + libpam-authentik = pkgs.callPackage ./nix/packages/libpam-authentik.nix { inherit version; - } - else - pkgs.runCommand "libpam-authentik-unsupported" {} '' - echo "libpam-authentik is only available on Linux" >&2 - exit 1 - ''; - - libnss-authentik = if isLinux then - pkgs.callPackage ./nix/packages/libnss-authentik.nix { + }; + libnss-authentik = pkgs.callPackage ./nix/packages/libnss-authentik.nix { inherit version; - } - else - pkgs.runCommand "libnss-authentik-unsupported" {} '' - echo "libnss-authentik is only available on Linux" >&2 - exit 1 - ''; - - default = self.packages.${system}.ak-agent; - }; - - devShells.default = pkgs.mkShell { - buildInputs = with pkgs; [ - go - gopls - gotools - go-tools - rustToolchain - rust-analyzer - protobuf - protoc-gen-go - protoc-gen-go-grpc - pkg-config - openssl - nodejs - gnumake - ] ++ lib.optionals isLinux [ - pam - ] ++ lib.optionals isDarwin [ - libiconv - ]; + }; + }; + packages = basePackages // linuxPackages; + in + { + inherit packages; + + apps.default = { + type = "app"; + program = "${packages.default}/bin/ak-agent"; + }; - shellHook = '' - echo "authentik platform development shell" - echo "Go: $(go version)" - echo "Rust: $(rustc --version)" - ''; - }; + 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)" + ''; + }; - # Expose individual package checks - checks = { - ak-cli = self.packages.${system}.ak-cli; - }; - } - ) // { - # NixOS module + formatter = pkgs.nixpkgs-fmt; + }) // { nixosModules.default = import ./nix/modules/authentik.nix; nixosModules.authentik = self.nixosModules.default; - # nix-darwin module darwinModules.default = import ./nix/modules/darwin.nix; darwinModules.authentik = self.darwinModules.default; - # Overlay for use in other flakes - overlays.default = final: prev: { - authentik-cli = self.packages.${prev.system}.ak-cli; - authentik-sysd = self.packages.${prev.system}.ak-sysd; - authentik-agent = self.packages.${prev.system}.ak-agent; - authentik-pam = self.packages.${prev.system}.libpam-authentik; - authentik-nss = self.packages.${prev.system}.libnss-authentik; - }; + overlays.default = mkOverlay; }; } diff --git a/nix/modules/darwin.nix b/nix/modules/darwin.nix index 582a9f09..46e0018e 100644 --- a/nix/modules/darwin.nix +++ b/nix/modules/darwin.nix @@ -4,7 +4,8 @@ with lib; let cfg = config.services.authentik; -in { +in +{ options.services.authentik = { enable = mkEnableOption "authentik platform agent"; @@ -21,6 +22,12 @@ in { 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 = [ ]; @@ -33,10 +40,10 @@ in { environment.systemPackages = [ cfg.package ]; # Copy .app bundle to /Applications and install browser native messaging hosts - system.activationScripts.postActivation.text = '' + system.activationScripts.authentik-agent.text = '' echo "Installing authentik Agent.app..." rm -rf "/Applications/authentik Agent.app" - cp -r "${cfg.package}/Applications/authentik Agent.app" "/Applications/" + cp -R "${cfg.package}/Applications/authentik Agent.app" "/Applications/" chmod -R 755 "/Applications/authentik Agent.app" mkdir -p /Library/Logs/io.goauthentik @@ -62,9 +69,15 @@ in { serviceConfig = { Label = "io.goauthentik.platform.sysd"; ProgramArguments = [ - "/Applications/authentik Agent.app/Contents/MacOS/ak-sysd" + "${cfg.package}/Applications/authentik Agent.app/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; diff --git a/nix/packages/ak-agent.nix b/nix/packages/ak-agent.nix index 31f7c853..405001ba 100644 --- a/nix/packages/ak-agent.nix +++ b/nix/packages/ak-agent.nix @@ -24,53 +24,56 @@ buildGoModule { 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 - '' + 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 + 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" + # 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" + # 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/" + # 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/" - # Copy Info.plist from source and substitute version - sed "s|0.35.2|${version}|g" \ - "${srcRoot}/cmd/agent_local/package/macos/authentik Agent.app/Contents/Info.plist" \ - > "$APP_DIR/Info.plist" + # 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" - ''; + # 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"; diff --git a/nix/packages/ak-cli.nix b/nix/packages/ak-cli.nix index ea87ad9f..c94afa8b 100644 --- a/nix/packages/ak-cli.nix +++ b/nix/packages/ak-cli.nix @@ -21,9 +21,8 @@ buildGoModule { postInstall = '' mv $out/bin/cli $out/bin/ak - # Create symlinks for subcommands + # Match the packaged CLI entrypoint aliases. ln -s $out/bin/ak $out/bin/ak-vault - ln -s $out/bin/ak $out/bin/ak-browser-support ''; meta = with lib; { From d96e61ae703dca6b08b32d6adc8f71cbbf227897 Mon Sep 17 00:00:00 2001 From: Dominic R Date: Sat, 7 Mar 2026 20:22:18 -0500 Subject: [PATCH 05/14] remove marker --- Makefile | 1 - 1 file changed, 1 deletion(-) diff --git a/Makefile b/Makefile index 158d18f6..32e30552 100644 --- a/Makefile +++ b/Makefile @@ -155,7 +155,6 @@ ee/psso/%: ee/wcp/%: "$(MAKE)" -C "${TOP}/ee/wcp/" $* -<<<<<<< HEAD containers/selenium/%: "$(MAKE)" -C "${TOP}/containers/selenium" $* From 4bcca7967f0a6db84d66285b0437ae9bd23ce22f Mon Sep 17 00:00:00 2001 From: Dominic R Date: Sat, 7 Mar 2026 21:12:39 -0500 Subject: [PATCH 06/14] Fixes the following: - ak-sysd only searched /Applications, so nix installs in /Applications/Nix Apps never launched the GUI agent. - The Darwin module relied on a flaky one-shot open instead of exposing the app at the upstream path. - The Darwin module seeded only the default config path, ignoring services.authentik.configFile. - The Darwin module missed the Chromium native-messaging manifest. - The browser extension native port could disconnect and fail signing requests. - Browser/native signing errors were swallowed and turned into useless empty responses. - ak-sysd challenge validation failed despite matching JWKS kid values because the key lookup path was broken. - Missing tests for macOS app path resolution and direct JWKS challenge validation were added. --- browser-ext/src/background/background.ts | 11 ++- browser-ext/src/content/content.ts | 18 +++++ browser-ext/src/utils/native.ts | 36 ++++++++- flake.nix | 7 ++ nix/modules/darwin.nix | 40 ++++++++-- pkg/agent_system/agent_starter/starter.go | 80 +++++++++++++++++-- .../agent_starter/starter_test.go | 52 ++++++++++++ pkg/agent_system/device/signed.go | 59 +++++++++----- pkg/agent_system/device/signed_test.go | 53 ++++++++++++ pkg/browser_support/handler.go | 13 ++- 10 files changed, 328 insertions(+), 41 deletions(-) create mode 100644 pkg/agent_system/agent_starter/starter_test.go create mode 100644 pkg/agent_system/device/signed_test.go diff --git a/browser-ext/src/background/background.ts b/browser-ext/src/background/background.ts index 5ec28df0..834ca1e1 100644 --- a/browser-ext/src/background/background.ts +++ b/browser-ext/src/background/background.ts @@ -3,6 +3,13 @@ import { sentry } from "../utils/sentry"; sentry("background"); +function stringifyError(exc: unknown): string { + if (exc instanceof Error) { + return exc.message; + } + return String(exc); +} + chrome.runtime.onInstalled.addListener(() => { console.debug("authentik Extension Installed"); }); @@ -19,7 +26,9 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { }) .catch((exc) => { console.warn("Failed to send request for platform sign", exc); - sendResponse(null); + sendResponse({ + error: stringifyError(exc), + }); }); break; } diff --git a/browser-ext/src/content/content.ts b/browser-ext/src/content/content.ts index 28d3d65a..6e05cd69 100644 --- a/browser-ext/src/content/content.ts +++ b/browser-ext/src/content/content.ts @@ -1,3 +1,13 @@ +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; +} + window.addEventListener( "message", (event) => { @@ -18,6 +28,14 @@ window.addEventListener( 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", diff --git a/browser-ext/src/utils/native.ts b/browser-ext/src/utils/native.ts index d5a8ee4d..f24b148e 100644 --- a/browser-ext/src/utils/native.ts +++ b/browser-ext/src/utils/native.ts @@ -11,6 +11,7 @@ export interface Message { export interface Response { response_to: string; data: { [key: string]: unknown }; + error?: string; } function createRandomString(length: number = 16) { @@ -31,6 +32,7 @@ export class Native { #promises: Map> = new Map(); #reconnectDelay = defaultReconnectDelay; #reconnectTimeout = 0; + #isConnected = false; constructor() { this.#connect(); @@ -38,8 +40,11 @@ export class Native { #connect() { this.#port = chrome.runtime.connectNative("io.goauthentik.platform"); + this.#isConnected = true; + this.#reconnectDelay = defaultReconnectDelay; this.#port.onMessage.addListener(this.#listener.bind(this)); this.#port.onDisconnect.addListener(() => { + this.#isConnected = false; this.#reconnectDelay *= 1.35; this.#reconnectDelay = Math.min(this.#reconnectDelay, 3600); // @ts-ignore @@ -48,6 +53,7 @@ export class Native { `authentik/bext/native: Disconnected, reconnecting in ${this.#reconnectDelay}`, err, ); + this.#port = undefined; clearTimeout(this.#reconnectTimeout); this.#reconnectTimeout = setTimeout(() => { this.#connect(); @@ -63,7 +69,35 @@ export class Native { console.debug(`authentik/bext/native[${msg.response_to}]: No promise to resolve`); return; } + if (msg.error) { + prom.reject(new Error(msg.error)); + this.#promises.delete(msg.response_to); + return; + } 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 { @@ -71,7 +105,7 @@ export class Native { const promise = Promise.withResolvers(); try { this.#promises.set(msg.id, promise); - this.#port?.postMessage(msg); + this.#postMessage(msg as Message, true); console.debug(`authentik/bext/native[${msg.id}]: Sending message ${msg.path}`); } catch (exc) { this.#promises.get(msg.id)?.reject(exc); diff --git a/flake.nix b/flake.nix index 51ef9dfa..f3664c6b 100644 --- a/flake.nix +++ b/flake.nix @@ -1,6 +1,13 @@ { 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"; diff --git a/nix/modules/darwin.nix b/nix/modules/darwin.nix index 46e0018e..182b19c9 100644 --- a/nix/modules/darwin.nix +++ b/nix/modules/darwin.nix @@ -4,6 +4,11 @@ with lib; let cfg = config.services.authentik; + appPath = "/Applications/authentik Agent.app"; + packageAppPath = "${cfg.package}/Applications/authentik Agent.app"; + 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 = { @@ -39,28 +44,47 @@ in # Install the package (makes binaries available in PATH) environment.systemPackages = [ cfg.package ]; - # Copy .app bundle to /Applications and install browser native messaging hosts + # 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 "/Applications/authentik Agent.app" - cp -R "${cfg.package}/Applications/authentik Agent.app" "/Applications/" - chmod -R 755 "/Applications/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" - cp "/Applications/authentik Agent.app/Contents/Resources/browser-host-chrome.json" \ + cp "${appPath}/Contents/Resources/browser-host-chrome.json" \ "/Library/Google/Chrome/NativeMessagingHosts/io.goauthentik.platform.json" + mkdir -p "/Library/Application Support/Chromium/NativeMessagingHosts" + cp "${appPath}/Contents/Resources/browser-host-chrome.json" \ + "/Library/Application Support/Chromium/NativeMessagingHosts/io.goauthentik.platform.json" + # Edge (system-wide) mkdir -p "/Library/Microsoft/Edge/NativeMessagingHosts" - cp "/Applications/authentik Agent.app/Contents/Resources/browser-host-chrome.json" \ + cp "${appPath}/Contents/Resources/browser-host-chrome.json" \ "/Library/Microsoft/Edge/NativeMessagingHosts/io.goauthentik.platform.json" # Firefox (system-wide) mkdir -p "/Library/Application Support/Mozilla/NativeMessagingHosts" - cp "/Applications/authentik Agent.app/Contents/Resources/browser-host-firefox.json" \ + cp "${appPath}/Contents/Resources/browser-host-firefox.json" \ "/Library/Application Support/Mozilla/NativeMessagingHosts/io.goauthentik.platform.json" ''; @@ -69,7 +93,7 @@ in serviceConfig = { Label = "io.goauthentik.platform.sysd"; ProgramArguments = [ - "${cfg.package}/Applications/authentik Agent.app/Contents/MacOS/ak-sysd" + "${packageAppPath}/Contents/MacOS/ak-sysd" "agent" ] ++ lib.optionals (cfg.configFile != null) [ "--config-file" 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/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..bcd260ba 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"` } @@ -83,7 +84,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 +99,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 +118,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{ From 8ca696098e42de8169745a8cec30837dac566a34 Mon Sep 17 00:00:00 2001 From: Dominic R Date: Sat, 7 Mar 2026 21:26:20 -0500 Subject: [PATCH 07/14] try to fix --- nix/modules/darwin.nix | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/nix/modules/darwin.nix b/nix/modules/darwin.nix index 182b19c9..0587d035 100644 --- a/nix/modules/darwin.nix +++ b/nix/modules/darwin.nix @@ -6,6 +6,7 @@ let cfg = config.services.authentik; appPath = "/Applications/authentik Agent.app"; packageAppPath = "${cfg.package}/Applications/authentik Agent.app"; + 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; @@ -70,22 +71,26 @@ in echo "Installing browser native messaging hosts..." # Chrome/Chromium (system-wide) mkdir -p "/Library/Google/Chrome/NativeMessagingHosts" - cp "${appPath}/Contents/Resources/browser-host-chrome.json" \ - "/Library/Google/Chrome/NativeMessagingHosts/io.goauthentik.platform.json" + sed "s|/Applications/authentik Agent.app/Contents/MacOS/ak-browser-support|${browserSupportPath}|g" \ + "${appPath}/Contents/Resources/browser-host-chrome.json" \ + > "/Library/Google/Chrome/NativeMessagingHosts/io.goauthentik.platform.json" mkdir -p "/Library/Application Support/Chromium/NativeMessagingHosts" - cp "${appPath}/Contents/Resources/browser-host-chrome.json" \ - "/Library/Application Support/Chromium/NativeMessagingHosts/io.goauthentik.platform.json" + sed "s|/Applications/authentik Agent.app/Contents/MacOS/ak-browser-support|${browserSupportPath}|g" \ + "${appPath}/Contents/Resources/browser-host-chrome.json" \ + > "/Library/Application Support/Chromium/NativeMessagingHosts/io.goauthentik.platform.json" # Edge (system-wide) mkdir -p "/Library/Microsoft/Edge/NativeMessagingHosts" - cp "${appPath}/Contents/Resources/browser-host-chrome.json" \ - "/Library/Microsoft/Edge/NativeMessagingHosts/io.goauthentik.platform.json" + sed "s|/Applications/authentik Agent.app/Contents/MacOS/ak-browser-support|${browserSupportPath}|g" \ + "${appPath}/Contents/Resources/browser-host-chrome.json" \ + > "/Library/Microsoft/Edge/NativeMessagingHosts/io.goauthentik.platform.json" # Firefox (system-wide) mkdir -p "/Library/Application Support/Mozilla/NativeMessagingHosts" - cp "${appPath}/Contents/Resources/browser-host-firefox.json" \ - "/Library/Application Support/Mozilla/NativeMessagingHosts/io.goauthentik.platform.json" + sed "s|/Applications/authentik Agent.app/Contents/MacOS/ak-browser-support|${browserSupportPath}|g" \ + "${appPath}/Contents/Resources/browser-host-firefox.json" \ + > "/Library/Application Support/Mozilla/NativeMessagingHosts/io.goauthentik.platform.json" ''; # Launchd daemon for ak-sysd (runs as root) From 28144673657f9353eeb541afec8ba3c735f8979a Mon Sep 17 00:00:00 2001 From: Dominic R Date: Sat, 7 Mar 2026 21:27:53 -0500 Subject: [PATCH 08/14] more --- browser-ext/src/content/content.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/browser-ext/src/content/content.ts b/browser-ext/src/content/content.ts index 6e05cd69..7122cf1d 100644 --- a/browser-ext/src/content/content.ts +++ b/browser-ext/src/content/content.ts @@ -17,6 +17,7 @@ window.addEventListener( event.data._ak_ext === "authentik-platform-sso" && event.data.challenge ) { + console.debug("authentik/bext: received endpoint challenge"); try { if (event.source !== window) { return; @@ -37,10 +38,19 @@ window.addEventListener( return; } if (signed) { + console.debug( + "authentik/bext: posting signed endpoint response back to page", + { + responseLength: + typeof signed === "string" ? signed.length : null, + }, + ); window.postMessage({ _ak_ext: "authentik-platform-sso", response: signed, - }); + }, window.location.origin); + } else { + console.warn("authentik/bext: background returned empty response"); } }); } catch (exc) { From 9c9e26455277e9d1084f9e044a01043b520363f7 Mon Sep 17 00:00:00 2001 From: Dominic R Date: Sat, 7 Mar 2026 21:29:53 -0500 Subject: [PATCH 09/14] try to return a promise directly --- browser-ext/src/background/background.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/browser-ext/src/background/background.ts b/browser-ext/src/background/background.ts index 834ca1e1..9a76b124 100644 --- a/browser-ext/src/background/background.ts +++ b/browser-ext/src/background/background.ts @@ -16,21 +16,19 @@ chrome.runtime.onInstalled.addListener(() => { const native = new Native(); -chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { +chrome.runtime.onMessage.addListener((msg) => { switch (msg.action) { case "platform_sign_endpoint_header": - native + return native .platformSignEndpointHeader(msg.profile, msg.challenge) - .then((r) => { - sendResponse(r); - }) + .then((r) => r) .catch((exc) => { console.warn("Failed to send request for platform sign", exc); - sendResponse({ + return { error: stringifyError(exc), - }); + }; }); - break; + default: + return false; } - return true; }); From 5a120664762eef9ecde3b937a4b46bc5df9429c9 Mon Sep 17 00:00:00 2001 From: Dominic R Date: Sat, 7 Mar 2026 21:32:12 -0500 Subject: [PATCH 10/14] compat stuff --- browser-ext/src/background/background.ts | 40 ++++++++++++++------ browser-ext/src/content/content.ts | 48 +++++++++++++++++++++++- browser-ext/src/utils/native.ts | 15 +++++--- 3 files changed, 85 insertions(+), 18 deletions(-) diff --git a/browser-ext/src/background/background.ts b/browser-ext/src/background/background.ts index 9a76b124..fd5a23a8 100644 --- a/browser-ext/src/background/background.ts +++ b/browser-ext/src/background/background.ts @@ -3,6 +3,9 @@ import { sentry } from "../utils/sentry"; sentry("background"); +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; @@ -16,19 +19,34 @@ chrome.runtime.onInstalled.addListener(() => { const native = new Native(); -chrome.runtime.onMessage.addListener((msg) => { +async function handleMessage(msg: { action?: string; profile?: string; challenge?: string }) { switch (msg.action) { case "platform_sign_endpoint_header": - return native - .platformSignEndpointHeader(msg.profile, msg.challenge) - .then((r) => r) - .catch((exc) => { - console.warn("Failed to send request for platform sign", exc); - return { - error: stringifyError(exc), - }; - }); + console.debug("authentik/bext/background: signing endpoint challenge"); + 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 7122cf1d..32204343 100644 --- a/browser-ext/src/content/content.ts +++ b/browser-ext/src/content/content.ts @@ -8,6 +8,50 @@ function stringifyError(value: unknown): string | null { 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) => { @@ -22,8 +66,8 @@ window.addEventListener( if (event.source !== window) { return; } - chrome.runtime - .sendMessage({ + console.debug("authentik/bext: sending challenge to background"); + sendRuntimeMessage({ action: "platform_sign_endpoint_header", profile: "default", challenge: event.data.challenge, diff --git a/browser-ext/src/utils/native.ts b/browser-ext/src/utils/native.ts index f24b148e..3b4eb2a6 100644 --- a/browser-ext/src/utils/native.ts +++ b/browser-ext/src/utils/native.ts @@ -14,6 +14,9 @@ export interface Response { 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 = ""; @@ -39,16 +42,18 @@ export class Native { } #connect() { - this.#port = chrome.runtime.connectNative("io.goauthentik.platform"); + const port = runtimeApi.connectNative("io.goauthentik.platform"); + this.#port = port; this.#isConnected = true; this.#reconnectDelay = defaultReconnectDelay; - this.#port.onMessage.addListener(this.#listener.bind(this)); - this.#port.onDisconnect.addListener(() => { + 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; + const err = + (typeof chrome !== "undefined" ? chrome.runtime?.lastError : undefined) || + (port as chrome.runtime.Port & { error?: unknown }).error; console.debug( `authentik/bext/native: Disconnected, reconnecting in ${this.#reconnectDelay}`, err, From 07aad5b0c38e81ecdbdf52f83878d2be7249492f Mon Sep 17 00:00:00 2001 From: Dominic R Date: Sat, 7 Mar 2026 21:32:45 -0500 Subject: [PATCH 11/14] return on disconn --- browser-ext/src/content/content.ts | 3 ++ browser-ext/src/utils/native.ts | 44 ++++++++++++++++++++++++++++-- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/browser-ext/src/content/content.ts b/browser-ext/src/content/content.ts index 32204343..ada73fbe 100644 --- a/browser-ext/src/content/content.ts +++ b/browser-ext/src/content/content.ts @@ -96,6 +96,9 @@ window.addEventListener( } 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 3b4eb2a6..932d95c7 100644 --- a/browser-ext/src/utils/native.ts +++ b/browser-ext/src/utils/native.ts @@ -29,10 +29,15 @@ 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; @@ -54,6 +59,11 @@ export class Native { 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)}` : ""}`, + ), + ); console.debug( `authentik/bext/native: Disconnected, reconnecting in ${this.#reconnectDelay}`, err, @@ -75,10 +85,16 @@ export class Native { 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); } @@ -109,15 +125,37 @@ export class Native { msg.id = createRandomString(); const promise = Promise.withResolvers(); try { - this.#promises.set(msg.id, promise); + 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); console.debug(`authentik/bext/native[${msg.id}]: Sending message ${msg.path}`); } 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", From 96987c0a6930258addf6905c437c123974250aba Mon Sep 17 00:00:00 2001 From: Dominic R Date: Sat, 7 Mar 2026 21:39:40 -0500 Subject: [PATCH 12/14] more fixes --- nix/modules/darwin.nix | 13 +++++--- pkg/agent_system/config/config.go | 3 +- pkg/agent_system/config/config_test.go | 41 ++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 6 deletions(-) create mode 100644 pkg/agent_system/config/config_test.go diff --git a/nix/modules/darwin.nix b/nix/modules/darwin.nix index 0587d035..b786141b 100644 --- a/nix/modules/darwin.nix +++ b/nix/modules/darwin.nix @@ -6,6 +6,7 @@ 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; @@ -72,25 +73,29 @@ in # Chrome/Chromium (system-wide) mkdir -p "/Library/Google/Chrome/NativeMessagingHosts" sed "s|/Applications/authentik Agent.app/Contents/MacOS/ak-browser-support|${browserSupportPath}|g" \ - "${appPath}/Contents/Resources/browser-host-chrome.json" \ + "${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" \ - "${appPath}/Contents/Resources/browser-host-chrome.json" \ + "${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" \ - "${appPath}/Contents/Resources/browser-host-chrome.json" \ + "${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" \ - "${appPath}/Contents/Resources/browser-host-firefox.json" \ + "${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) 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) +} From ccf46bc605bc12b1f6e4455ba0e4fb7231fca91d Mon Sep 17 00:00:00 2001 From: Dominic R Date: Sat, 7 Mar 2026 21:48:09 -0500 Subject: [PATCH 13/14] this might be an authentik bug --- browser-ext/src/content/content.ts | 95 ++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/browser-ext/src/content/content.ts b/browser-ext/src/content/content.ts index ada73fbe..3cf726dd 100644 --- a/browser-ext/src/content/content.ts +++ b/browser-ext/src/content/content.ts @@ -10,6 +10,99 @@ function stringifyError(value: unknown): string | null { const browserApi = (globalThis as typeof globalThis & { browser?: typeof chrome }).browser; const runtimeApi = browserApi?.runtime ?? chrome.runtime; +const debugScriptId = "authentik-platform-sso-debug"; + +function injectPageDebugScript() { + if (document.getElementById(debugScriptId)) { + return; + } + const script = document.createElement("script"); + script.id = debugScriptId; + script.textContent = ` +(() => { + if (window.__akPlatformDebugInstalled) { + return; + } + window.__akPlatformDebugInstalled = true; + const summarizeBody = (init) => { + const body = init?.body; + if (!body || typeof body !== "string") { + return null; + } + try { + const parsed = JSON.parse(body); + return { + component: parsed?.component ?? null, + hasResponse: typeof parsed?.response === "string", + hasSelectedChallenge: Boolean(parsed?.selectedChallenge), + selectedDeviceClass: parsed?.selectedChallenge?.deviceClass ?? null, + selectedDeviceUid: parsed?.selectedChallenge?.deviceUid ?? null, + hasWebauthn: Boolean(parsed?.webauthn), + hasDuo: typeof parsed?.duo !== "undefined", + hasCode: typeof parsed?.code !== "undefined", + }; + } catch { + return { rawLength: body.length }; + } + }; + const origFetch = window.fetch.bind(window); + window.fetch = async (...args) => { + const input = args[0]; + const init = args[1]; + const url = typeof input === "string" ? input : input instanceof Request ? input.url : String(input); + const method = init?.method ?? (input instanceof Request ? input.method : "GET"); + const isFlowExecutor = url.includes("/api/v3/flows/executor/"); + if (!isFlowExecutor) { + return origFetch(...args); + } + const started = performance.now(); + console.debug("authentik/bext/page: fetch start", { + url, + method, + request: summarizeBody(init), + }); + try { + const response = await origFetch(...args); + const clone = response.clone(); + let body = null; + try { + body = await clone.json(); + } catch { + body = null; + } + console.debug("authentik/bext/page: fetch done", { + url, + method, + status: response.status, + ok: response.ok, + elapsedMs: Math.round(performance.now() - started), + component: body?.component ?? null, + responseErrors: body?.responseErrors ?? null, + flowInfoTitle: body?.flowInfo?.title ?? null, + hasChallenge: Boolean(body?.challenge), + deviceClasses: Array.isArray(body?.deviceChallenges) + ? body.deviceChallenges.map((challenge) => challenge?.deviceClass ?? null) + : null, + deviceUids: Array.isArray(body?.deviceChallenges) + ? body.deviceChallenges.map((challenge) => challenge?.deviceUid ?? null) + : null, + }); + return response; + } catch (error) { + console.warn("authentik/bext/page: fetch failed", { + url, + method, + elapsedMs: Math.round(performance.now() - started), + error: error instanceof Error ? error.message : String(error), + }); + throw error; + } + }; +})(); +`; + (document.documentElement || document.head || document.body).appendChild(script); + script.remove(); +} function sendRuntimeMessage(message: { action: string; @@ -52,6 +145,8 @@ function sendRuntimeMessage(message: { }); } +injectPageDebugScript(); + window.addEventListener( "message", (event) => { From 89822847edf4d6df892ed9a6a034003317c8b559 Mon Sep 17 00:00:00 2001 From: Dominic R Date: Sat, 7 Mar 2026 21:55:04 -0500 Subject: [PATCH 14/14] remove some debug stuff --- browser-ext/src/background/background.ts | 5 -- browser-ext/src/content/content.ts | 104 ----------------------- browser-ext/src/utils/native.ts | 8 -- pkg/browser_support/handler.go | 1 - 4 files changed, 118 deletions(-) diff --git a/browser-ext/src/background/background.ts b/browser-ext/src/background/background.ts index fd5a23a8..2c3822f9 100644 --- a/browser-ext/src/background/background.ts +++ b/browser-ext/src/background/background.ts @@ -13,16 +13,11 @@ function stringifyError(exc: unknown): string { return String(exc); } -chrome.runtime.onInstalled.addListener(() => { - console.debug("authentik Extension Installed"); -}); - const native = new Native(); async function handleMessage(msg: { action?: string; profile?: string; challenge?: string }) { switch (msg.action) { case "platform_sign_endpoint_header": - console.debug("authentik/bext/background: signing endpoint challenge"); try { return await native.platformSignEndpointHeader(msg.profile ?? "default", msg.challenge ?? ""); } catch (exc) { diff --git a/browser-ext/src/content/content.ts b/browser-ext/src/content/content.ts index 3cf726dd..1e0df467 100644 --- a/browser-ext/src/content/content.ts +++ b/browser-ext/src/content/content.ts @@ -10,99 +10,6 @@ function stringifyError(value: unknown): string | null { const browserApi = (globalThis as typeof globalThis & { browser?: typeof chrome }).browser; const runtimeApi = browserApi?.runtime ?? chrome.runtime; -const debugScriptId = "authentik-platform-sso-debug"; - -function injectPageDebugScript() { - if (document.getElementById(debugScriptId)) { - return; - } - const script = document.createElement("script"); - script.id = debugScriptId; - script.textContent = ` -(() => { - if (window.__akPlatformDebugInstalled) { - return; - } - window.__akPlatformDebugInstalled = true; - const summarizeBody = (init) => { - const body = init?.body; - if (!body || typeof body !== "string") { - return null; - } - try { - const parsed = JSON.parse(body); - return { - component: parsed?.component ?? null, - hasResponse: typeof parsed?.response === "string", - hasSelectedChallenge: Boolean(parsed?.selectedChallenge), - selectedDeviceClass: parsed?.selectedChallenge?.deviceClass ?? null, - selectedDeviceUid: parsed?.selectedChallenge?.deviceUid ?? null, - hasWebauthn: Boolean(parsed?.webauthn), - hasDuo: typeof parsed?.duo !== "undefined", - hasCode: typeof parsed?.code !== "undefined", - }; - } catch { - return { rawLength: body.length }; - } - }; - const origFetch = window.fetch.bind(window); - window.fetch = async (...args) => { - const input = args[0]; - const init = args[1]; - const url = typeof input === "string" ? input : input instanceof Request ? input.url : String(input); - const method = init?.method ?? (input instanceof Request ? input.method : "GET"); - const isFlowExecutor = url.includes("/api/v3/flows/executor/"); - if (!isFlowExecutor) { - return origFetch(...args); - } - const started = performance.now(); - console.debug("authentik/bext/page: fetch start", { - url, - method, - request: summarizeBody(init), - }); - try { - const response = await origFetch(...args); - const clone = response.clone(); - let body = null; - try { - body = await clone.json(); - } catch { - body = null; - } - console.debug("authentik/bext/page: fetch done", { - url, - method, - status: response.status, - ok: response.ok, - elapsedMs: Math.round(performance.now() - started), - component: body?.component ?? null, - responseErrors: body?.responseErrors ?? null, - flowInfoTitle: body?.flowInfo?.title ?? null, - hasChallenge: Boolean(body?.challenge), - deviceClasses: Array.isArray(body?.deviceChallenges) - ? body.deviceChallenges.map((challenge) => challenge?.deviceClass ?? null) - : null, - deviceUids: Array.isArray(body?.deviceChallenges) - ? body.deviceChallenges.map((challenge) => challenge?.deviceUid ?? null) - : null, - }); - return response; - } catch (error) { - console.warn("authentik/bext/page: fetch failed", { - url, - method, - elapsedMs: Math.round(performance.now() - started), - error: error instanceof Error ? error.message : String(error), - }); - throw error; - } - }; -})(); -`; - (document.documentElement || document.head || document.body).appendChild(script); - script.remove(); -} function sendRuntimeMessage(message: { action: string; @@ -145,8 +52,6 @@ function sendRuntimeMessage(message: { }); } -injectPageDebugScript(); - window.addEventListener( "message", (event) => { @@ -156,12 +61,10 @@ window.addEventListener( event.data._ak_ext === "authentik-platform-sso" && event.data.challenge ) { - console.debug("authentik/bext: received endpoint challenge"); try { if (event.source !== window) { return; } - console.debug("authentik/bext: sending challenge to background"); sendRuntimeMessage({ action: "platform_sign_endpoint_header", profile: "default", @@ -177,13 +80,6 @@ window.addEventListener( return; } if (signed) { - console.debug( - "authentik/bext: posting signed endpoint response back to page", - { - responseLength: - typeof signed === "string" ? signed.length : null, - }, - ); window.postMessage({ _ak_ext: "authentik-platform-sso", response: signed, diff --git a/browser-ext/src/utils/native.ts b/browser-ext/src/utils/native.ts index 932d95c7..36ec799d 100644 --- a/browser-ext/src/utils/native.ts +++ b/browser-ext/src/utils/native.ts @@ -64,24 +64,17 @@ export class Native { `native host disconnected${err ? `: ${String(err)}` : ""}`, ), ); - console.debug( - `authentik/bext/native: Disconnected, reconnecting in ${this.#reconnectDelay}`, - 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) { @@ -134,7 +127,6 @@ export class Native { }, requestTimeoutMs); this.#promises.set(msg.id, pending); this.#postMessage(msg as Message, true); - console.debug(`authentik/bext/native[${msg.id}]: Sending message ${msg.path}`); } catch (exc) { const pending = this.#promises.get(msg.id); if (pending?.timeout) { diff --git a/pkg/browser_support/handler.go b/pkg/browser_support/handler.go index bcd260ba..38f24f1b 100644 --- a/pkg/browser_support/handler.go +++ b/pkg/browser_support/handler.go @@ -75,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,