From 8f4c24119e465d5fd9b11c334922d2e10d3c2a2e Mon Sep 17 00:00:00 2001 From: fuomag9 <1580624+fuomag9@users.noreply.github.com> Date: Fri, 17 Apr 2026 10:11:24 +0200 Subject: [PATCH] Add excluded paths support for forward auth (fixes #108) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Allow users to exclude specific paths from Authentik/CPM forward auth protection. When excluded_paths is set, all paths require authentication EXCEPT the excluded ones — useful for apps like Navidrome that need /share/* and /rest/* to bypass auth. Co-Authored-By: Claude Opus 4.6 (1M context) --- app/(dashboard)/proxy-hosts/actions.ts | 8 + app/api/v1/openapi.json/route.ts | 1 + .../proxy-hosts/AuthentikFields.tsx | 13 ++ .../proxy-hosts/CpmForwardAuthFields.tsx | 13 ++ src/lib/caddy.ts | 103 ++++++++++++- src/lib/models/proxy-hosts.ts | 33 ++++- .../forward-auth-excluded-paths.spec.ts | 139 ++++++++++++++++++ tests/integration/proxy-hosts-meta.test.ts | 72 +++++++++ 8 files changed, 376 insertions(+), 6 deletions(-) create mode 100644 tests/e2e/functional/forward-auth-excluded-paths.spec.ts diff --git a/app/(dashboard)/proxy-hosts/actions.ts b/app/(dashboard)/proxy-hosts/actions.ts index a25eaf6f..1c26cb36 100644 --- a/app/(dashboard)/proxy-hosts/actions.ts +++ b/app/(dashboard)/proxy-hosts/actions.ts @@ -77,6 +77,7 @@ function parseAuthentikConfig(formData: FormData): ProxyHostAuthentikInput | und const copyHeaders = parseCsv(formData.get("authentik_copy_headers")); const trustedProxies = parseCsv(formData.get("authentik_trusted_proxies")); const protectedPaths = parseCsv(formData.get("authentik_protected_paths")); + const excludedPaths = parseCsv(formData.get("authentik_excluded_paths")); const setHostHeader = formData.has("authentik_set_host_header_present") ? parseCheckbox(formData.get("authentik_set_host_header")) : undefined; @@ -103,6 +104,9 @@ function parseAuthentikConfig(formData: FormData): ProxyHostAuthentikInput | und if (protectedPaths.length > 0 || formData.has("authentik_protected_paths")) { result.protectedPaths = protectedPaths; } + if (excludedPaths.length > 0 || formData.has("authentik_excluded_paths")) { + result.excludedPaths = excludedPaths; + } if (setHostHeader !== undefined) { result.setOutpostHostHeader = setHostHeader; } @@ -122,6 +126,7 @@ function parseCpmForwardAuthConfig(formData: FormData): CpmForwardAuthInput | un : false : undefined; const protectedPaths = parseCsv(formData.get("cpm_forward_auth_protected_paths")); + const excludedPaths = parseCsv(formData.get("cpm_forward_auth_excluded_paths")); const result: CpmForwardAuthInput = {}; if (enabledValue !== undefined) { @@ -130,6 +135,9 @@ function parseCpmForwardAuthConfig(formData: FormData): CpmForwardAuthInput | un if (protectedPaths.length > 0 || formData.has("cpm_forward_auth_protected_paths")) { result.protected_paths = protectedPaths.length > 0 ? protectedPaths : null; } + if (excludedPaths.length > 0 || formData.has("cpm_forward_auth_excluded_paths")) { + result.excluded_paths = excludedPaths.length > 0 ? excludedPaths : null; + } return Object.keys(result).length > 0 ? result : undefined; } diff --git a/app/api/v1/openapi.json/route.ts b/app/api/v1/openapi.json/route.ts index 92ba74a4..a37f3eb1 100644 --- a/app/api/v1/openapi.json/route.ts +++ b/app/api/v1/openapi.json/route.ts @@ -1448,6 +1448,7 @@ const spec = { trustedProxies: { type: "array", items: { type: "string" }, example: ["private_ranges"] }, setOutpostHostHeader: { type: "boolean" }, protectedPaths: { type: ["array", "null"], items: { type: "string" }, description: "Paths to protect (null = all)" }, + excludedPaths: { type: ["array", "null"], items: { type: "string" }, description: "Paths to exclude from auth (bypassed while rest is protected)" }, }, }, LoadBalancerConfig: { diff --git a/src/components/proxy-hosts/AuthentikFields.tsx b/src/components/proxy-hosts/AuthentikFields.tsx index ed1c5abe..f2b950b9 100644 --- a/src/components/proxy-hosts/AuthentikFields.tsx +++ b/src/components/proxy-hosts/AuthentikFields.tsx @@ -162,6 +162,19 @@ export function AuthentikFields({ Leave empty to protect entire domain. Specify paths to protect specific routes only.

+
+ +