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.
+
+
Excluded Paths (Optional)
+
+
+ Paths to exclude from authentication. These paths will bypass forward auth while all other paths remain protected. Ignored if Protected Paths is set.
+
+
+
+
Excluded Paths (Optional)
+
+
+ Paths to exclude from authentication. These paths will bypass forward auth while all other paths remain protected. Ignored if Protected Paths is set.
+
+
{/* Allowed Groups */}
{groups.length > 0 && (
diff --git a/src/lib/caddy.ts b/src/lib/caddy.ts
index 47e71a75..30543a4f 100644
--- a/src/lib/caddy.ts
+++ b/src/lib/caddy.ts
@@ -109,6 +109,7 @@ type UpstreamDnsResolutionMeta = {
type CpmForwardAuthMeta = {
enabled?: boolean;
protected_paths?: string[];
+ excluded_paths?: string[];
};
type ProxyHostMeta = {
@@ -144,6 +145,7 @@ type ProxyHostAuthentikMeta = {
trusted_proxies?: string[];
set_outpost_host_header?: boolean;
protected_paths?: string[];
+ excluded_paths?: string[];
};
type AuthentikRouteConfig = {
@@ -155,6 +157,7 @@ type AuthentikRouteConfig = {
trustedProxies: string[];
setOutpostHostHeader: boolean;
protectedPaths: string[] | null;
+ excludedPaths: string[] | null;
};
type LoadBalancerActiveHealthCheckMeta = {
@@ -1030,6 +1033,7 @@ async function buildProxyRoutes(
// Path-based authentication support
if (authentik.protectedPaths && authentik.protectedPaths.length > 0) {
+ // Whitelist mode: only specified paths get auth
for (const domainGroup of domainGroups) {
// Create separate routes for each protected path
for (const protectedPath of authentik.protectedPaths) {
@@ -1087,7 +1091,54 @@ async function buildProxyRoutes(
terminal: true
});
}
+ } else if (authentik.excludedPaths && authentik.excludedPaths.length > 0) {
+ // Exclusion mode: protect everything EXCEPT specified paths
+ const locationRules = meta.location_rules ?? [];
+ for (const domainGroup of domainGroups) {
+ if (outpostRoute) {
+ const outpostMatches = (outpostRoute.match as Array> | undefined) ?? [];
+ hostRoutes.push({
+ ...outpostRoute,
+ match: outpostMatches.map((match) => ({
+ ...match,
+ host: domainGroup
+ }))
+ });
+ }
+
+ // Create unprotected routes for each excluded path (before the catch-all)
+ for (const excludedPath of authentik.excludedPaths) {
+ hostRoutes.push({
+ match: [{ host: domainGroup, path: [excludedPath] }],
+ handle: [...handlers, JSON.parse(JSON.stringify(reverseProxyHandler))],
+ terminal: true
+ });
+ }
+
+ // Location rules get auth (same as full-site mode)
+ for (const rule of locationRules) {
+ const { safePath, reverseProxyHandler: locationProxy } = buildLocationReverseProxy(
+ rule,
+ Boolean(row.skipHttpsHostnameValidation),
+ Boolean(row.preserveHostHeader)
+ );
+ if (!safePath) continue;
+ hostRoutes.push({
+ match: [{ host: domainGroup, path: [safePath] }],
+ handle: [...handlers, forwardAuthHandler, locationProxy],
+ terminal: true,
+ });
+ }
+
+ // Catch-all with auth (everything not excluded)
+ hostRoutes.push({
+ match: [{ host: domainGroup }],
+ handle: [...handlers, forwardAuthHandler, reverseProxyHandler],
+ terminal: true
+ });
+ }
} else {
+ // Full-site mode: protect everything
const locationRules = meta.location_rules ?? [];
for (const domainGroup of domainGroups) {
if (outpostRoute) {
@@ -1229,7 +1280,7 @@ async function buildProxyRoutes(
const locationRules = meta.location_rules ?? [];
if (cpmForwardAuth.protected_paths && cpmForwardAuth.protected_paths.length > 0) {
- // Path-specific authentication
+ // Whitelist mode: only specified paths get auth
for (const domainGroup of domainGroups) {
// Add callback route (unprotected)
hostRoutes.push({
@@ -1273,8 +1324,48 @@ async function buildProxyRoutes(
terminal: true
});
}
+ } else if (cpmForwardAuth.excluded_paths && cpmForwardAuth.excluded_paths.length > 0) {
+ // Exclusion mode: protect everything EXCEPT specified paths
+ for (const domainGroup of domainGroups) {
+ // Callback route first (unprotected)
+ hostRoutes.push({
+ ...cpmCallbackRoute,
+ match: [{ host: domainGroup, path: ["/.cpm-auth/callback"] }]
+ });
+
+ // Excluded paths — unprotected, before the catch-all
+ for (const excludedPath of cpmForwardAuth.excluded_paths) {
+ hostRoutes.push({
+ match: [{ host: domainGroup, path: [excludedPath] }],
+ handle: [...handlers, JSON.parse(JSON.stringify(reverseProxyHandler))],
+ terminal: true
+ });
+ }
+
+ // Location rules with forward auth
+ for (const rule of locationRules) {
+ const { safePath, reverseProxyHandler: locationProxy } = buildLocationReverseProxy(
+ rule,
+ Boolean(row.skipHttpsHostnameValidation),
+ Boolean(row.preserveHostHeader)
+ );
+ if (!safePath) continue;
+ hostRoutes.push({
+ match: [{ host: domainGroup, path: [safePath] }],
+ handle: [...handlers, cpmForwardAuthHandler, locationProxy],
+ terminal: true
+ });
+ }
+
+ // Catch-all with auth (everything not excluded)
+ hostRoutes.push({
+ match: [{ host: domainGroup }],
+ handle: [...handlers, cpmForwardAuthHandler, reverseProxyHandler],
+ terminal: true
+ });
+ }
} else {
- // Protect entire site
+ // Full-site mode: protect everything
for (const domainGroup of domainGroups) {
// Callback route first (unprotected)
hostRoutes.push({
@@ -2282,6 +2373,11 @@ function parseAuthentikConfig(meta: ProxyHostAuthentikMeta | undefined | null):
? meta.protected_paths.map((path) => path?.trim()).filter((path): path is string => Boolean(path))
: null;
+ const excludedPaths =
+ Array.isArray(meta.excluded_paths) && meta.excluded_paths.length > 0
+ ? meta.excluded_paths.map((path) => path?.trim()).filter((path): path is string => Boolean(path))
+ : null;
+
return {
enabled: true,
outpostDomain,
@@ -2290,7 +2386,8 @@ function parseAuthentikConfig(meta: ProxyHostAuthentikMeta | undefined | null):
copyHeaders,
trustedProxies,
setOutpostHostHeader,
- protectedPaths
+ protectedPaths,
+ excludedPaths
};
}
diff --git a/src/lib/models/proxy-hosts.ts b/src/lib/models/proxy-hosts.ts
index b9c30464..2bf09656 100644
--- a/src/lib/models/proxy-hosts.ts
+++ b/src/lib/models/proxy-hosts.ts
@@ -208,6 +208,7 @@ export type ProxyHostAuthentikConfig = {
trustedProxies: string[];
setOutpostHostHeader: boolean;
protectedPaths: string[] | null;
+ excludedPaths: string[] | null;
};
export type ProxyHostAuthentikInput = {
@@ -219,6 +220,7 @@ export type ProxyHostAuthentikInput = {
trustedProxies?: string[] | null;
setOutpostHostHeader?: boolean | null;
protectedPaths?: string[] | null;
+ excludedPaths?: string[] | null;
};
type ProxyHostAuthentikMeta = {
@@ -230,6 +232,7 @@ type ProxyHostAuthentikMeta = {
trusted_proxies?: string[];
set_outpost_host_header?: boolean;
protected_paths?: string[];
+ excluded_paths?: string[];
};
export type MtlsConfig = {
@@ -245,16 +248,19 @@ export type MtlsConfig = {
export type CpmForwardAuthConfig = {
enabled: boolean;
protected_paths: string[] | null;
+ excluded_paths: string[] | null;
};
export type CpmForwardAuthInput = {
enabled?: boolean;
protected_paths?: string[] | null;
+ excluded_paths?: string[] | null;
};
type CpmForwardAuthMeta = {
enabled?: boolean;
protected_paths?: string[];
+ excluded_paths?: string[];
};
type ProxyHostMeta = {
@@ -806,6 +812,17 @@ function normalizeAuthentikInput(
}
}
+ if (input.excludedPaths !== undefined) {
+ const paths = (input.excludedPaths ?? [])
+ .map((path) => path?.trim())
+ .filter((path): path is string => Boolean(path));
+ if (paths.length > 0) {
+ next.excluded_paths = paths;
+ } else {
+ delete next.excluded_paths;
+ }
+ }
+
if ((next.enabled ?? false) && next.outpost_domain && !next.auth_endpoint) {
next.auth_endpoint = `/${next.outpost_domain}/auth/caddy`;
}
@@ -1198,6 +1215,9 @@ function buildMeta(existing: ProxyHostMeta, input: Partial): str
if (input.cpmForwardAuth.protected_paths && input.cpmForwardAuth.protected_paths.length > 0) {
cfa.protected_paths = input.cpmForwardAuth.protected_paths;
}
+ if (input.cpmForwardAuth.excluded_paths && input.cpmForwardAuth.excluded_paths.length > 0) {
+ cfa.excluded_paths = input.cpmForwardAuth.excluded_paths;
+ }
next.cpm_forward_auth = cfa;
} else {
delete next.cpm_forward_auth;
@@ -1254,6 +1274,8 @@ function hydrateAuthentik(meta: ProxyHostAuthentikMeta | undefined): ProxyHostAu
meta.set_outpost_host_header !== undefined ? Boolean(meta.set_outpost_host_header) : true;
const protectedPaths =
Array.isArray(meta.protected_paths) && meta.protected_paths.length > 0 ? meta.protected_paths : null;
+ const excludedPaths =
+ Array.isArray(meta.excluded_paths) && meta.excluded_paths.length > 0 ? meta.excluded_paths : null;
return {
enabled,
@@ -1263,7 +1285,8 @@ function hydrateAuthentik(meta: ProxyHostAuthentikMeta | undefined): ProxyHostAu
copyHeaders,
trustedProxies,
setOutpostHostHeader,
- protectedPaths
+ protectedPaths,
+ excludedPaths
};
}
@@ -1295,6 +1318,9 @@ function dehydrateAuthentik(config: ProxyHostAuthentikConfig | null): ProxyHostA
if (config.protectedPaths && config.protectedPaths.length > 0) {
meta.protected_paths = [...config.protectedPaths];
}
+ if (config.excludedPaths && config.excludedPaths.length > 0) {
+ meta.excluded_paths = [...config.excludedPaths];
+ }
return meta;
}
@@ -1559,7 +1585,7 @@ function parseProxyHost(row: ProxyHostRow): ProxyHost {
waf: meta.waf ?? null,
mtls: meta.mtls ?? null,
cpmForwardAuth: meta.cpm_forward_auth?.enabled
- ? { enabled: true, protected_paths: meta.cpm_forward_auth.protected_paths ?? null }
+ ? { enabled: true, protected_paths: meta.cpm_forward_auth.protected_paths ?? null, excluded_paths: meta.cpm_forward_auth.excluded_paths ?? null }
: null,
redirects: meta.redirects ?? [],
rewrite: meta.rewrite ?? null,
@@ -1702,7 +1728,8 @@ export async function updateProxyHost(id: number, input: Partial
...(existing.cpmForwardAuth?.enabled ? {
cpm_forward_auth: {
enabled: true,
- ...(existing.cpmForwardAuth.protected_paths ? { protected_paths: existing.cpmForwardAuth.protected_paths } : {})
+ ...(existing.cpmForwardAuth.protected_paths ? { protected_paths: existing.cpmForwardAuth.protected_paths } : {}),
+ ...(existing.cpmForwardAuth.excluded_paths ? { excluded_paths: existing.cpmForwardAuth.excluded_paths } : {})
}
} : {}),
};
diff --git a/tests/e2e/functional/forward-auth-excluded-paths.spec.ts b/tests/e2e/functional/forward-auth-excluded-paths.spec.ts
new file mode 100644
index 00000000..5e2fb435
--- /dev/null
+++ b/tests/e2e/functional/forward-auth-excluded-paths.spec.ts
@@ -0,0 +1,139 @@
+/**
+ * Functional tests: CPM Forward Auth with excluded paths.
+ *
+ * Creates a proxy host with CPM forward auth enabled and excluded_paths set,
+ * then verifies:
+ * - Excluded paths bypass auth and reach the upstream directly
+ * - Non-excluded paths still require authentication (redirect to portal)
+ * - The callback route still works for completing auth on non-excluded paths
+ *
+ * This validates the fix for GitHub issue #108: the ability to exclude
+ * specific paths from forward auth (e.g., /share/*, /rest/* for Navidrome).
+ *
+ * Domain: func-fwd-auth-excl.test
+ */
+import { test, expect } from '@playwright/test';
+import { httpGet, waitForStatus } from '../../helpers/http';
+
+const DOMAIN = 'func-fwd-auth-excl.test';
+const ECHO_BODY = 'echo-ok';
+const BASE_URL = 'http://localhost:3000';
+const API = `${BASE_URL}/api/v1`;
+
+let proxyHostId: number;
+
+test.describe.serial('Forward Auth Excluded Paths', () => {
+ test('setup: create proxy host with forward auth and excluded paths via API', async ({ page }) => {
+ const res = await page.request.post(`${API}/proxy-hosts`, {
+ data: {
+ name: 'Excluded Paths Test',
+ domains: [DOMAIN],
+ upstreams: ['echo-server:8080'],
+ sslForced: false,
+ cpmForwardAuth: {
+ enabled: true,
+ excluded_paths: ['/share/*', '/rest/*'],
+ },
+ },
+ headers: { 'Content-Type': 'application/json', 'Origin': BASE_URL },
+ });
+ expect(res.status()).toBe(201);
+ const host = await res.json();
+ proxyHostId = host.id;
+
+ // Grant testadmin (user ID 1) forward auth access
+ const accessRes = await page.request.put(`${API}/proxy-hosts/${proxyHostId}/forward-auth-access`, {
+ data: { userIds: [1], groupIds: [] },
+ headers: { 'Content-Type': 'application/json', 'Origin': BASE_URL },
+ });
+ expect(accessRes.status()).toBe(200);
+
+ // Wait for Caddy to pick up the config — non-excluded paths should redirect (302)
+ await waitForStatus(DOMAIN, 302, 20_000);
+ });
+
+ test('excluded path /share/* bypasses auth and reaches upstream', async () => {
+ const res = await httpGet(DOMAIN, '/share/some-track');
+ expect(res.status).toBe(200);
+ expect(res.body).toContain(ECHO_BODY);
+ });
+
+ test('excluded path /rest/* bypasses auth and reaches upstream', async () => {
+ const res = await httpGet(DOMAIN, '/rest/ping');
+ expect(res.status).toBe(200);
+ expect(res.body).toContain(ECHO_BODY);
+ });
+
+ test('non-excluded root path requires auth (redirects to portal)', async () => {
+ const res = await httpGet(DOMAIN, '/');
+ expect(res.status).toBe(302);
+ const location = String(res.headers['location']);
+ expect(location).toContain('/portal?rd=');
+ expect(location).toContain(DOMAIN);
+ });
+
+ test('non-excluded arbitrary path requires auth', async () => {
+ const res = await httpGet(DOMAIN, '/admin/dashboard');
+ expect(res.status).toBe(302);
+ expect(String(res.headers['location'])).toContain('/portal');
+ });
+
+ test('credential login works for non-excluded paths', async ({ page }) => {
+ const context = await page.context().browser()!.newContext({ storageState: { cookies: [], origins: [] } });
+ const freshPage = await context.newPage();
+
+ try {
+ await freshPage.goto(`${BASE_URL}/portal?rd=http://${DOMAIN}/protected-page`);
+ await expect(freshPage.getByLabel('Username')).toBeVisible({ timeout: 10_000 });
+
+ // Intercept the login API response
+ let capturedRedirect: string | null = null;
+ await freshPage.route('**/api/forward-auth/login', async (route) => {
+ const response = await route.fetch();
+ const json = await response.json();
+ capturedRedirect = json.redirectTo ?? null;
+ await route.fulfill({ response });
+ });
+
+ await freshPage.getByLabel('Username').fill('testadmin');
+ await freshPage.getByLabel('Password').fill('TestPassword2026!');
+ await freshPage.getByRole('button', { name: 'Sign in', exact: true }).click();
+
+ const deadline = Date.now() + 15_000;
+ while (!capturedRedirect && Date.now() < deadline) {
+ await freshPage.waitForTimeout(200);
+ }
+
+ expect(capturedRedirect).toBeTruthy();
+ expect(capturedRedirect).toContain('/.cpm-auth/callback');
+
+ // Complete the callback
+ const callbackUrl = new URL(capturedRedirect!);
+ const callbackRes = await httpGet(DOMAIN, callbackUrl.pathname + callbackUrl.search);
+ expect(callbackRes.status).toBe(302);
+ const setCookie = String(callbackRes.headers['set-cookie'] ?? '');
+ expect(setCookie).toContain('_cpm_fa=');
+
+ // Verify authenticated access to non-excluded path
+ const match = setCookie.match(/_cpm_fa=([^;]+)/);
+ expect(match).toBeTruthy();
+ const sessionCookie = match![1];
+ const upstreamRes = await httpGet(DOMAIN, '/protected-page', {
+ Cookie: `_cpm_fa=${sessionCookie}`,
+ });
+ expect(upstreamRes.status).toBe(200);
+ expect(upstreamRes.body).toContain(ECHO_BODY);
+ } finally {
+ await context.close();
+ }
+ });
+
+ test('cleanup: delete proxy host', async ({ page }) => {
+ if (proxyHostId) {
+ const res = await page.request.delete(`${API}/proxy-hosts/${proxyHostId}`, {
+ headers: { 'Origin': BASE_URL },
+ });
+ expect(res.status()).toBe(200);
+ }
+ });
+});
diff --git a/tests/integration/proxy-hosts-meta.test.ts b/tests/integration/proxy-hosts-meta.test.ts
index 619a145f..3ff91904 100644
--- a/tests/integration/proxy-hosts-meta.test.ts
+++ b/tests/integration/proxy-hosts-meta.test.ts
@@ -217,6 +217,78 @@ describe('proxy-hosts boolean fields', () => {
});
});
+// ---------------------------------------------------------------------------
+// Authentik forward auth meta round-trip
+// ---------------------------------------------------------------------------
+
+describe('proxy-hosts authentik meta', () => {
+ it('stores and retrieves authentik config with excluded_paths', async () => {
+ const meta = {
+ authentik: {
+ enabled: true,
+ outpost_domain: 'outpost.goauthentik.io',
+ outpost_upstream: 'http://authentik:9000',
+ excluded_paths: ['/share/*', '/rest/*'],
+ },
+ };
+ const host = await insertHost({ meta: JSON.stringify(meta) });
+ const row = await db.query.proxyHosts.findFirst({ where: (t, { eq }) => eq(t.id, host.id) });
+ const parsed = JSON.parse(row!.meta!);
+ expect(parsed.authentik.enabled).toBe(true);
+ expect(parsed.authentik.excluded_paths).toEqual(['/share/*', '/rest/*']);
+ });
+
+ it('stores authentik config with protected_paths (no excluded_paths)', async () => {
+ const meta = {
+ authentik: {
+ enabled: true,
+ outpost_domain: 'outpost.goauthentik.io',
+ outpost_upstream: 'http://authentik:9000',
+ protected_paths: ['/admin/*', '/secret/*'],
+ },
+ };
+ const host = await insertHost({ meta: JSON.stringify(meta) });
+ const row = await db.query.proxyHosts.findFirst({ where: (t, { eq }) => eq(t.id, host.id) });
+ const parsed = JSON.parse(row!.meta!);
+ expect(parsed.authentik.protected_paths).toEqual(['/admin/*', '/secret/*']);
+ expect(parsed.authentik.excluded_paths).toBeUndefined();
+ });
+});
+
+// ---------------------------------------------------------------------------
+// CPM forward auth meta round-trip
+// ---------------------------------------------------------------------------
+
+describe('proxy-hosts CPM forward auth meta', () => {
+ it('stores and retrieves cpm_forward_auth config with excluded_paths', async () => {
+ const meta = {
+ cpm_forward_auth: {
+ enabled: true,
+ excluded_paths: ['/api/public/*', '/health'],
+ },
+ };
+ const host = await insertHost({ meta: JSON.stringify(meta) });
+ const row = await db.query.proxyHosts.findFirst({ where: (t, { eq }) => eq(t.id, host.id) });
+ const parsed = JSON.parse(row!.meta!);
+ expect(parsed.cpm_forward_auth.enabled).toBe(true);
+ expect(parsed.cpm_forward_auth.excluded_paths).toEqual(['/api/public/*', '/health']);
+ });
+
+ it('stores cpm_forward_auth config with protected_paths (no excluded_paths)', async () => {
+ const meta = {
+ cpm_forward_auth: {
+ enabled: true,
+ protected_paths: ['/admin/*'],
+ },
+ };
+ const host = await insertHost({ meta: JSON.stringify(meta) });
+ const row = await db.query.proxyHosts.findFirst({ where: (t, { eq }) => eq(t.id, host.id) });
+ const parsed = JSON.parse(row!.meta!);
+ expect(parsed.cpm_forward_auth.protected_paths).toEqual(['/admin/*']);
+ expect(parsed.cpm_forward_auth.excluded_paths).toBeUndefined();
+ });
+});
+
// ---------------------------------------------------------------------------
// Null meta field
// ---------------------------------------------------------------------------