C2: Build authentik from source (Mikado chain) (#274)
All checks were successful
Build Container / detect (push) Successful in 3s
Build Container (Nix) / detect (push) Successful in 1s
Build Container / build (authentik) (push) Successful in 2s
Build Container (Nix) / build (authentik) (push) Successful in 22s

## Mikado Chain: build-authentik-from-source

Replace `pkgs.authentik` from nixpkgs with a custom Nix derivation built from source.
This removes the dependency on the nixpkgs packaging timeline and gives full version control.

Target version: **2025.12.4** (nixpkgs reference, upgrading from deployed 2025.10.1).

### Dependency Graph

```
build-authentik-from-source (goal)
├── authentik-go-server-derivation
│   ├── authentik-api-client-generation  ← IN PROGRESS
│   └── authentik-python-backend-derivation
├── authentik-web-ui-derivation
│   └── authentik-api-client-generation  ← IN PROGRESS
└── authentik-python-backend-derivation
```

### Ready Leaves
- `authentik-api-client-generation` — Go + TypeScript client generation from OpenAPI schema
- `authentik-python-backend-derivation` — Django backend with 60+ deps, 4 in-tree packages

### Architecture
Ported from [nixpkgs `pkgs/by-name/au/authentik/package.nix`](https://github.com/NixOS/nixpkgs/tree/master/pkgs/by-name/au/authentik):
- `source.nix` — shared version/source fetch
- `client-go.nix` — Go API client generation
- `client-ts.nix` — TypeScript API client generation
- `api-go-vendor-hook.nix` — Go vendor directory injection hook
- (more components to follow as leaves are closed)

### Related Cards
- [[build-authentik-from-source]] — Goal card
- [[authentik-api-client-generation]]
- [[authentik-python-backend-derivation]]
- [[authentik-web-ui-derivation]]
- [[authentik-go-server-derivation]]

Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/274
This commit is contained in:
Erich Blume 2026-03-01 13:45:00 -08:00
commit efa9806bfa
21 changed files with 868 additions and 85 deletions

View file

@ -0,0 +1,28 @@
# Setup hook that injects generated Go API client into the vendor directory
# Replaces vendor/goauthentik.io/api/v3/ with freshly generated client-go output
# Skips during FOD (fixed-output derivation) builds to keep vendorHash stable
{ pkgs ? import <nixpkgs> { }, sources ? import ./sources.nix { inherit pkgs; } }:
let
client-go = import ./client-go.nix { inherit pkgs sources; };
in
pkgs.makeSetupHook
{
name = "authentik-api-go-vendor-hook";
}
(
pkgs.writeShellScript "authentik-api-go-vendor-hook" ''
authentikApiGoVendorHook() {
chmod -R +w vendor/goauthentik.io/api
rm -rf vendor/goauthentik.io/api/v3
cp -r ${client-go} vendor/goauthentik.io/api/v3
echo "Finished authentikApiGoVendorHook"
}
# don't run for FOD, e.g. the goModules build
if [ -z ''${outputHash-} ]; then
postConfigureHooks+=(authentikApiGoVendorHook)
fi
''
)

View file

@ -0,0 +1,153 @@
# Authentik Python/Django backend
#
# Assembles the final package from:
# 1. python-deps FOD (venv with stripped store references)
# 2. opencontainers git dependency (fetched via Nix)
# 3. Workspace packages (ak-guardian, django-channels-postgres, etc.)
# 4. Authentik application source
# 5. Lifecycle scripts, blueprints, manage.py
#
# autoPatchelfHook restores RPATHs that were stripped in the FOD.
#
# Optional input: webui derivation. When provided, resolves @webui@ store
# path placeholders in Python source. When null (default), leaves placeholders
# for isolated testing.
#
# Output:
# $out/bin/python3.14 venv python (symlink to nix python314)
# $out/lib/python3.14/site-packages/ all Python packages
# $out/lifecycle/ lifecycle scripts (symlink)
# $out/blueprints/ YAML blueprints
# $out/manage.py Django management script
{ pkgs ? import <nixpkgs> { }
, sources ? import ./sources.nix { inherit pkgs; }
, webui ? null
}:
let
python-deps = import ./python-deps.nix { inherit pkgs sources; };
# opencontainers is a git dependency not on PyPI — fetch separately
opencontainers-src = pkgs.fetchFromGitHub {
owner = "vsoch";
repo = "oci-python";
rev = "ceb4fcc090851717a3069d78e85ceb1e86c2740c";
hash = "sha256-Q6SJed0K6eIrqQ9mNAD4RGx+YCJvnI5E+0KGp5fBtTU=";
};
# When webui is provided, resolve paths directly; otherwise use placeholder
webuiPath = if webui != null then "${webui}" else "@webui@";
sp = "$out/lib/python3.14/site-packages";
in
pkgs.stdenv.mkDerivation {
pname = "authentik-django";
version = sources.version;
inherit (sources) meta;
src = sources.src;
nativeBuildInputs = with pkgs; [
autoPatchelfHook # restores RPATHs stripped in the FOD
];
# Libraries that autoPatchelfHook resolves NEEDED entries against
buildInputs = with pkgs; [
python314
stdenv.cc.cc.lib # libstdc++, libgcc_s
libxml2
libxslt
xmlsec
openssl
libpq
krb5.lib
libtool.lib
libffi
zlib
];
dontBuild = true;
installPhase = ''
runHook preInstall
# --- Copy venv from FOD ---
cp -r ${python-deps} $out
chmod -R +w $out
# Restore python path in pyvenv.cfg (was replaced with @python@ in FOD)
sed -i "s|@python@|${pkgs.python314}|g" $out/pyvenv.cfg
# Recreate bin/ (was removed in FOD to strip python store refs)
mkdir -p $out/bin
ln -s ${pkgs.python314}/bin/python3.14 $out/bin/python3.14
ln -s python3.14 $out/bin/python3
ln -s python3.14 $out/bin/python
# Recreate entry point scripts that were in the venv's bin/
# (gunicorn, etc. — use python from this venv)
for ep in gunicorn uvicorn dramatiq dumb-init; do
if [ -e ${sp}/$ep ] || $out/bin/python3.14 -c "import $ep" 2>/dev/null; then
cat > $out/bin/$ep << SCRIPT
#!$out/bin/python3.14
import sys
from importlib.metadata import entry_points
eps = entry_points(group='console_scripts', name='$ep')
if eps:
sys.exit(eps[0].load()())
SCRIPT
chmod +x $out/bin/$ep
fi
done 2>/dev/null || true
# --- opencontainers (git dependency, pure Python) ---
cp -r ${opencontainers-src}/opencontainers ${sp}/opencontainers
# --- Workspace packages (pure Python — direct copy) ---
# ak-guardian: hatch config maps to "guardian" package
cp -r packages/ak-guardian/guardian ${sp}/guardian
cp -r packages/django-channels-postgres/django_channels_postgres ${sp}/
cp -r packages/django-dramatiq-postgres/django_dramatiq_postgres ${sp}/
cp -r packages/django-postgres-cache/django_postgres_cache ${sp}/
# --- Authentik application + lifecycle ---
cp -r authentik ${sp}/authentik
cp -r lifecycle ${sp}/lifecycle
chmod +x ${sp}/lifecycle/ak
# --- Patches for Nix store paths ---
# BASE_DIR: point to $out instead of computing from settings.py's location
substituteInPlace ${sp}/authentik/root/settings.py \
--replace-fail \
'BASE_DIR = Path(__file__).absolute().parent.parent.parent' \
"BASE_DIR = Path(\"$out\")"
# blueprints_dir: point to $out/blueprints
substituteInPlace ${sp}/authentik/lib/default.yml \
--replace-fail 'blueprints_dir: /blueprints' \
"blueprints_dir: $out/blueprints"
# Web asset paths: placeholder @webui@ for Go server card to resolve
substituteInPlace ${sp}/authentik/stages/email/utils.py \
--replace-fail 'Path("web/icons/icon_left_brand.png")' \
'Path("${webuiPath}/icons/icon_left_brand.png")' \
--replace-fail 'Path("web/dist/assets/icons/icon_left_brand.png")' \
'Path("${webuiPath}/dist/assets/icons/icon_left_brand.png")'
# Lifecycle bash script: use Nix store bash (no /usr/bin/env in containers)
substituteInPlace ${sp}/lifecycle/ak \
--replace-fail '#!/usr/bin/env -S bash' '#!${pkgs.bash}/bin/bash'
# --- Top-level structure ---
ln -s ${sp}/lifecycle $out/lifecycle
cp -r blueprints $out/blueprints
cp manage.py $out/manage.py
runHook postInstall
'';
# autoPatchelfHook runs in fixupPhase — don't disable it
dontPatchShebangs = true;
}

View file

@ -0,0 +1,64 @@
# Authentik Go HTTP server binary
#
# Builds cmd/server from the authentik source using buildGoModule.
# The compiled binary serves the web UI, REST API, spawns gunicorn
# for the Django backend, and runs the embedded reverse proxy outpost.
#
# Two runtime path dependencies are baked in at compile time:
# - authentik-django: lifecycle scripts (gunicorn launcher)
# - webui: static web assets (dist/ and authentik/ directories)
#
# The apiGoVendorHook replaces vendored goauthentik.io/api/v3 with
# freshly generated client-go output, but only during the real build
# (not the FOD module-download phase), so vendorHash stays stable.
#
# Output: $out/bin/authentik
{ pkgs ? import <nixpkgs> { }
, sources ? import ./sources.nix { inherit pkgs; }
, authentik-django ? import ./authentik-django.nix { inherit pkgs sources; }
, webui ? null
}:
let
apiGoVendorHook = import ./api-go-vendor-hook.nix { inherit pkgs sources; };
# Web assets path: use real webui derivation if provided, otherwise
# a placeholder directory. The placeholder allows the binary to compile
# and pass --help verification, but web serving won't work at runtime.
webAssetsPath =
if webui != null then webui
else pkgs.runCommand "webui-placeholder" { } ''
mkdir -p $out/dist $out/authentik
'';
in
pkgs.buildGoModule {
pname = "authentik-server";
inherit (sources) version src meta;
subPackages = [ "cmd/server" ];
nativeBuildInputs = [ apiGoVendorHook ];
env.CGO_ENABLED = 0;
postPatch = ''
substituteInPlace internal/gounicorn/gounicorn.go \
--replace-fail './lifecycle' "${authentik-django}/lifecycle"
substituteInPlace web/static.go \
--replace-fail './web' "${webAssetsPath}"
substituteInPlace internal/web/static.go \
--replace-fail './web' "${webAssetsPath}"
'';
# Clear postPatch during the module-download FOD phase so that
# substituteInPlace (which references authentik-django and webui
# store paths) doesn't affect vendorHash computation.
overrideModAttrs.postPatch = "";
vendorHash = "sha256-bdILiCQgDuzp+VJDVW3z2JxTtxlHkm9tmMHiA/Sx6ts=";
postInstall = ''
mv $out/bin/server $out/bin/authentik
'';
}

View file

@ -0,0 +1,47 @@
# Generate Go API client bindings from authentik's OpenAPI schema
# Uses openapi-generator-cli to produce Go code from schema.yml
{ pkgs ? import <nixpkgs> { }, sources ? import ./sources.nix { inherit pkgs; } }:
pkgs.stdenvNoCC.mkDerivation {
pname = "authentik-client-go";
version = "3.${sources.version}";
inherit (sources) meta;
src = sources.client-go-src;
# Docker volume path /local → local pwd
postPatch = ''
substituteInPlace ./config.yaml \
--replace-fail '/local' "$(pwd)"
'';
nativeBuildInputs = with pkgs; [
openapi-generator-cli
go
];
buildPhase = ''
runHook preBuild
openapi-generator-cli generate \
-i ${sources.src}/schema.yml -o $out \
-g go \
-c ./config.yaml
gofmt -w $out
runHook postBuild
'';
installPhase = ''
runHook preInstall
cp go.mod go.sum $out
cd $out
rm -rf test
rm -f .travis.yml git_push.sh
runHook postInstall
'';
}

View file

@ -0,0 +1,36 @@
# Generate TypeScript fetch client bindings from authentik's OpenAPI schema
# Uses openapi-generator-cli to produce TypeScript code, then compiles with tsc
{ pkgs ? import <nixpkgs> { }, sources ? import ./sources.nix { inherit pkgs; } }:
pkgs.stdenvNoCC.mkDerivation {
pname = "authentik-client-ts";
inherit (sources) version src meta;
# Docker volume path /local → local pwd
postPatch = ''
substituteInPlace ./scripts/api/ts-config.yaml \
--replace-fail '/local' "$(pwd)"
'';
nativeBuildInputs = with pkgs; [
nodejs
openapi-generator-cli
typescript
];
buildPhase = ''
runHook preBuild
openapi-generator-cli generate \
-i ./schema.yml -o $out \
-g typescript-fetch \
-c ./scripts/api/ts-config.yaml \
--additional-properties=npmVersion=${sources.version} \
--git-repo-id authentik --git-user-id goauthentik
cd $out
npm run build
runHook postBuild
'';
}

View file

@ -1,19 +1,41 @@
# Nix-built Authentik identity provider
# Uses nixpkgs authentik package (ak entrypoint wrapping Go server + Python worker)
# Built with dockerTools.buildLayeredImage for efficient layer caching
# Nix-built Authentik identity provider (from source)
#
# Assembles four component derivations into a container image:
# 1. webui — Lit frontend (esbuild + rollup)
# 2. authentik-django — Python backend + lifecycle scripts
# 3. authentik-server — Go HTTP server binary
# 4. ak wrapper — sets PATH/VIRTUAL_ENV, delegates to lifecycle/ak
#
# Built with dockerTools.buildLayeredImage for efficient layer caching.
{ pkgs ? import <nixpkgs> { } }:
let
# Wrapper entrypoint that sets up /blueprints symlinks before running ak.
# buildLayeredImage's extraCommands can't access store paths from contents (they're
# in separate layers), so we create the symlinks at container start instead.
sources = import ./sources.nix { inherit pkgs; };
# Duplicated from sources.nix so build-container-nix.yaml can grep it
version = "2026.2.0";
webui = import ./webui.nix { inherit pkgs sources; };
authentik-django = import ./authentik-django.nix { inherit pkgs sources webui; };
authentik-server = import ./authentik-server.nix { inherit pkgs sources authentik-django webui; };
# Wrapper that provides bin/ak with the correct runtime environment.
# lifecycle/ak dispatches: "server" → Go binary, "worker"/"migrate"/etc → Python.
ak = pkgs.writeShellScriptBin "ak" ''
export PYTHONDONTWRITEBYTECODE=1
export PATH="${authentik-server}/bin:${authentik-django}/bin:$PATH"
export VIRTUAL_ENV="${authentik-django}"
cd "${authentik-django}"
exec "${authentik-django}/lifecycle/ak" "$@"
'';
# Container entrypoint: symlink built-in blueprints then run ak.
# buildLayeredImage's extraCommands can't access store paths from contents
# (they're in separate layers), so we create the symlinks at container start.
entrypoint = pkgs.writeShellScript "authentik-entrypoint" ''
# Link built-in blueprint dirs from the Nix store into /blueprints
for item in /nix/store/*authentik-django*/blueprints/*/; do
for item in ${authentik-django}/blueprints/*/; do
name=$(basename "$item")
[ ! -e "/blueprints/$name" ] && ln -s "$item" "/blueprints/$name" 2>/dev/null || true
done
exec ${pkgs.authentik}/bin/ak "$@"
exec ${ak}/bin/ak "$@"
'';
in
@ -22,7 +44,9 @@ pkgs.dockerTools.buildLayeredImage {
tag = "latest";
contents = [
pkgs.authentik
ak
authentik-django
authentik-server
pkgs.bashInteractive
pkgs.coreutils
pkgs.cacert
@ -30,9 +54,8 @@ pkgs.dockerTools.buildLayeredImage {
];
# Create /blueprints as world-writable so user 65534 can create symlinks at runtime.
# The nixpkgs authentik-django package hardcodes blueprints_dir to its Nix store path,
# making custom blueprints mounted at /blueprints/custom invisible. The entrypoint
# wrapper populates this directory with symlinks to built-in blueprints on each start.
# authentik-django hardcodes blueprints_dir to $out/blueprints; the AUTHENTIK_BLUEPRINTS_DIR
# env var overrides it to /blueprints, where custom blueprints are mounted by k8s ConfigMap.
extraCommands = ''
mkdir -p blueprints
chmod 777 blueprints

View file

@ -0,0 +1,125 @@
# Fixed-output derivation (FOD): download and install all external Python
# dependencies into a venv using uv sync.
#
# FODs get network access because the output hash is declared upfront.
# However, FODs must not reference other Nix store paths in their output.
# Compiled .so files (from sdist builds) contain RPATHs to system libraries
# (libxml2, krb5, etc.) which are Nix store paths. We strip these references
# here; authentik-django.nix restores them via autoPatchelfHook.
#
# The venv's bin/ and pyvenv.cfg also reference the python store path, so we
# replace them with placeholders that the main derivation restores.
#
# When uv.lock changes, reset outputHash to pkgs.lib.fakeHash, build to
# get the correct hash from the error message, then update.
{ pkgs ? import <nixpkgs> { }, sources ? import ./sources.nix { inherit pkgs; } }:
pkgs.stdenv.mkDerivation {
pname = "authentik-python-deps";
version = sources.version;
src = sources.src;
nativeBuildInputs = with pkgs; [
python314
uv
git # opencontainers is a git dependency in uv.lock
cacert # HTTPS verification for PyPI + GitHub
pkg-config
removeReferencesTo
# Build tools on PATH for sdist compilation
postgresql.pg_config # pg_config for psycopg-c
krb5 # krb5-config for gssapi
];
# System libraries for packages that must build from sdist:
# lxml, xmlsec — pyproject.toml [tool.uv] no-binary-package
# psycopg-c — sdist only on PyPI
# gssapi — no Linux wheels on PyPI
buildInputs = with pkgs; [
libxml2
libxslt
xmlsec
openssl
libpq # psycopg-c links against libpq
libtool # libltdl for xmlsec dynamic crypto backend loading
libffi
zlib
];
buildPhase = ''
runHook preBuild
export HOME=$TMPDIR
export SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt
export GIT_SSL_CAINFO=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt
export UV_PYTHON=${pkgs.python314}/bin/python3.14
export UV_LINK_MODE=copy
# gssapi's pre-generated C code uses S4U functions declared in gssapi_ext.h
# but doesn't include it — force-include via compiler flag
export NIX_CFLAGS_COMPILE="''${NIX_CFLAGS_COMPILE:-} -include gssapi/gssapi_ext.h"
uv sync \
--frozen \
--no-install-project \
--no-install-workspace \
--no-dev
runHook postBuild
'';
installPhase = ''
runHook preInstall
mv .venv $out
# --- Strip Nix store references (FODs must be self-contained) ---
# autoPatchelfHook in authentik-django.nix restores correct RPATHs.
# Replace python store path in pyvenv.cfg with placeholder
sed -i "s|${pkgs.python314}|@python@|g" $out/pyvenv.cfg
# Remove bin/ entirely — main derivation recreates it
rm -rf $out/bin
# Strip store refs from .pyc files (contain embedded paths)
find $out -type f -name '*.pyc' -delete
# Dynamically discover ALL remaining Nix store paths in the output.
# This is more robust than a static list of store paths — any new
# build/runtime dependency is automatically handled.
# Note: || true needed because xargs returns 123 if grep returns 1
# (no match) on any batch, and pipefail propagates that.
{ find $out -type f -print0 \
| xargs -0 grep -aohE '/nix/store/[a-z0-9]{32}-[^/"[:space:]]+' 2>/dev/null \
|| true; } | sort -u > $TMPDIR/store-refs.txt
echo "Found $(wc -l < $TMPDIR/store-refs.txt) unique store path references to strip"
# Build remove-references-to args from discovered paths
refs_args=""
while IFS= read -r ref; do
refs_args="$refs_args -t $ref"
done < $TMPDIR/store-refs.txt
# Strip all discovered references from all files
if [ -n "$refs_args" ]; then
find $out -type f -exec remove-references-to $refs_args {} + 2>/dev/null || true
fi
# Verify — report any remaining references
remaining=$({ find $out -type f -print0 | xargs -0 grep -cl '/nix/store/' 2>/dev/null || true; } | wc -l)
echo "Files with remaining store references: $remaining"
if [ "$remaining" -gt 0 ]; then
echo "WARNING: Files still containing store references:"
{ find $out -type f -print0 | xargs -0 grep -l '/nix/store/' 2>/dev/null || true; }
fi
runHook postInstall
'';
outputHashMode = "recursive";
outputHashAlgo = "sha256";
outputHash = "sha256-DtpcYQyI07m7v84D/UC28Tj35R9wye6IX+1D0gMZPgY=";
dontFixup = true;
}

View file

@ -0,0 +1,30 @@
# Centralized version and source pinning for authentik 2026.2.0
# All sources fetched from forge mirrors for supply chain control
{ pkgs ? import <nixpkgs> { } }:
let
version = "2026.2.0";
in
{
inherit version;
# Main authentik repo — provides schema.yml, Python backend, web UI, Go server
src = pkgs.fetchgit {
url = "https://forge.ops.eblu.me/mirrors/authentik.git";
rev = "version/${version}";
hash = "sha256-pVQ34cZYX3hlk6hF1aZ/n32xMqTF4Jmp0G0VGDU7iXc=";
};
# Go API client repo — provides config.yaml, go.mod, go.sum, templates
client-go-src = pkgs.fetchgit {
url = "https://forge.ops.eblu.me/mirrors/authentik-client-go.git";
rev = "v3.${version}";
hash = "sha256-DwXw/0QcSDYQKVhPA8tStrSoZooriQex/9FxSJtR/QY=";
};
meta = with pkgs.lib; {
description = "Authentik identity provider";
homepage = "https://goauthentik.io";
license = licenses.mit;
};
}

View file

@ -0,0 +1,44 @@
# Test harness for building authentik components on ringtail
# Uses builtins.getFlake instead of <nixpkgs> (ringtail has flakes, no NIX_PATH)
#
# Usage:
# nix-build test-build.nix -A python-deps --extra-experimental-features 'nix-command flakes'
# nix-build test-build.nix -A authentik-django --extra-experimental-features 'nix-command flakes'
# nix-build test-build.nix -A client-go --extra-experimental-features 'nix-command flakes'
# nix-build test-build.nix -A client-ts --extra-experimental-features 'nix-command flakes'
# nix-build test-build.nix -A authentik-server --extra-experimental-features 'nix-command flakes'
# nix-build test-build.nix -A webui-deps --extra-experimental-features 'nix-command flakes'
# nix-build test-build.nix -A webui --extra-experimental-features 'nix-command flakes'
# nix-build test-build.nix -A assembled --extra-experimental-features 'nix-command flakes'
let
pkgs = (builtins.getFlake "nixpkgs").legacyPackages.x86_64-linux;
sources = import ./sources.nix { inherit pkgs; };
# Individual components (isolated, no cross-wiring)
_webui = import ./webui.nix { inherit pkgs sources; };
# Fully wired assembly (webui → authentik-django → authentik-server)
_authentik-django-assembled = import ./authentik-django.nix { inherit pkgs sources; webui = _webui; };
_authentik-server-assembled = import ./authentik-server.nix {
inherit pkgs sources;
authentik-django = _authentik-django-assembled;
webui = _webui;
};
in
{
# Individual component builds (for debugging in isolation)
python-deps = import ./python-deps.nix { inherit pkgs sources; };
authentik-django = import ./authentik-django.nix { inherit pkgs sources; };
client-go = import ./client-go.nix { inherit pkgs sources; };
client-ts = import ./client-ts.nix { inherit pkgs sources; };
authentik-server = import ./authentik-server.nix { inherit pkgs sources; };
webui-deps = import ./webui-deps.nix { inherit pkgs sources; };
webui = _webui;
# Fully assembled stack — tests that all components wire together
assembled = pkgs.linkFarm "authentik-assembled-${sources.version}" [
{ name = "authentik-django"; path = _authentik-django-assembled; }
{ name = "authentik-server"; path = _authentik-server-assembled; }
{ name = "webui"; path = _webui; }
];
}

View file

@ -0,0 +1,51 @@
# Fixed-output derivation for authentik web UI npm dependencies
#
# Runs `npm ci` in the web/ directory to fetch all Node.js dependencies.
# This is a FOD (fixed-output derivation) so it has network access during build
# but the output hash must match exactly.
#
# The output hash is platform-specific because npm downloads platform-specific
# native binaries for esbuild, rollup, and SWC.
#
# Workspace packages (under web/packages/*) have their own node_modules,
# so we collect all node_modules directories via find.
#
# Output: all node_modules directories from the web/ tree
{ pkgs ? import <nixpkgs> { }, sources ? import ./sources.nix { inherit pkgs; } }:
pkgs.stdenvNoCC.mkDerivation {
pname = "authentik-webui-deps";
inherit (sources) version src meta;
sourceRoot = "${sources.src.name}/web";
outputHash =
{
"x86_64-linux" = "sha256-+4cWvFuixCcO7P+z701/0H+Ah/Z5sbLNsdx2Uowqwf4=";
}
.${pkgs.stdenvNoCC.hostPlatform.system}
or (throw "authentik-webui-deps: unsupported host platform ${pkgs.stdenvNoCC.hostPlatform.system}");
outputHashMode = "recursive";
nativeBuildInputs = with pkgs; [
nodejs_24
cacert
];
buildPhase = ''
npm ci --cache ./cache --ignore-scripts
rm -r ./cache node_modules/.package-lock.json
'';
# Workspace packages install dependencies into separate node_modules
# directories with symlinks between them — copy all of them
installPhase = ''
mkdir $out
find -type d -name node_modules -prune -print \
-exec mkdir -p $out/{} \; \
-exec cp -rT {} $out/{} \;
'';
dontCheckForBrokenSymlinks = true;
dontPatchShebangs = true;
}

View file

@ -0,0 +1,80 @@
# Authentik web UI build
#
# Builds the Lit-based TypeScript frontend from the web/ directory.
# Uses esbuild (via wireit) for the main build and rollup for the SFE
# (Standalone Frontend Engine) sub-package.
#
# Inputs:
# - webui-deps: FOD with npm dependencies (node_modules trees)
# - client-ts: generated TypeScript API client from schema.yml
#
# Output:
# $out/dist/ esbuild bundle (admin, user, flow, rac, etc.)
# $out/authentik/ static icons for authentication sources/connectors
{ pkgs ? import <nixpkgs> { }
, sources ? import ./sources.nix { inherit pkgs; }
, webui-deps ? import ./webui-deps.nix { inherit pkgs sources; }
, client-ts ? import ./client-ts.nix { inherit pkgs sources; }
}:
pkgs.stdenvNoCC.mkDerivation {
pname = "authentik-webui";
inherit (sources) version src meta;
sourceRoot = "${sources.src.name}/web";
nativeBuildInputs = with pkgs; [
nodejs_24
];
# Hardcode version string instead of importing from package.json
# (the JSON import-with-assertion may not resolve in the Nix build sandbox)
postPatch = ''
substituteInPlace packages/core/version/node.js \
--replace-fail \
'import PackageJSON from "../../../../package.json" with { type: "json" };' \
"" \
--replace-fail \
'(PackageJSON.version);' \
'"${sources.version}";'
'';
buildPhase = ''
runHook preBuild
# Copy node_modules from the FOD into the build tree
buildRoot=$PWD
pushd ${webui-deps}
find -type d -name node_modules -prune -print \
-exec cp -rT {} $buildRoot/{} \;
popd
# Replace the npm-published @goauthentik/api with our generated client
chmod -R +w node_modules/@goauthentik
rm -rf node_modules/@goauthentik/api
ln -sn ${client-ts} node_modules/@goauthentik/api
# Patch shebangs on build tool binaries so they can run in the sandbox
pushd node_modules/.bin
for tool in rollup wireit lit-localize esbuild; do
[ -L "$tool" ] && patchShebangs "$(readlink "$tool")" 2>/dev/null || true
done
popd
npm run build
npm run build:sfe
runHook postBuild
'';
installPhase = ''
runHook preInstall
mkdir $out
cp -r dist $out/dist
cp -r authentik $out/authentik
runHook postInstall
'';
NODE_ENV = "production";
NODE_OPTIONS = "--openssl-legacy-provider";
}