import { expect, test, vi } from "vitest"; import { chmodSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { stringify } from "yaml"; import { buildApp } from "../src/app.js"; import { loadConfig } from "../src/config.js"; test("configured OIDC advertises login but fails closed without its runtime client secret", async () => { const directory = mkdtempSync(join(realpathSync(tmpdir()), "thothii-app-oidc-mode-")); chmodSync(directory, 0o700); const file = join(directory, "auth.yaml"); writeFileSync(file, stringify({ version: 1, mode: "oidc", publicUrl: "https://thothii.example.org", oidc: { issuer: "https://authentik.example.org/application/o/thothii/", clientId: "thothii", clientSecretRef: "THT_OIDC_CLIENT_SECRET", scopes: ["openid"], groupsClaim: "groups", }, groupCatalog: { driver: "authentik", baseUrl: "https://authentik.example.org", apiTokenRef: "THT_AUTHENTIK_API_TOKEN" }, authorization: { groupRoles: { "TOT Users": ["user"], "TOT Admin": ["admin"] } }, }), { encoding: "utf8", mode: 0o600 }); chmodSync(file, 0o600); try { const app = buildApp(loadConfig({ THT_AUTH_CONFIG_FILE: file, THT_AUTH_STATE_ROOT: join(directory, "auth-state") })); try { expect((await app.inject({ method: "GET", url: "/auth/config" })).json()) .toEqual({ mode: "oidc", localLogin: false, oidcLogin: true }); const placeholder = await app.inject({ method: "GET", url: "/auth/oidc/login" }); expect(placeholder.statusCode).toBe(503); expect(placeholder.json()).toEqual({ code: "auth_unavailable", error: "Authentication is unavailable" }); } finally { await app.close(); } } finally { rmSync(directory, { recursive: true, force: true }); } }); test("configured OIDC initializes login from the literal secret bundle without an environment duplicate", async () => { const directory = mkdtempSync(join(realpathSync(tmpdir()), "thothii-app-oidc-bundle-")); chmodSync(directory, 0o700); const file = join(directory, "auth.yaml"); const bundle = join(directory, "thothii.secrets"); const clientSecret = "bundle-only-oidc-client-secret"; writeFileSync(file, stringify({ version: 1, mode: "oidc", publicUrl: "https://thothii.example.org", oidc: { issuer: "https://authentik.example.org/application/o/thothii/", clientId: "thothii", clientSecretRef: "THT_OIDC_CLIENT_SECRET", scopes: ["openid"], groupsClaim: "groups", }, groupCatalog: { driver: "authentik", baseUrl: "https://authentik.example.org", apiTokenRef: "THT_AUTHENTIK_API_TOKEN" }, authorization: { groupRoles: { "TOT Users": ["user"], "TOT Admin": ["admin"] } }, }), { encoding: "utf8", mode: 0o600 }); writeFileSync(bundle, `THT_OIDC_CLIENT_SECRET=${clientSecret}\nTHT_AUTHENTIK_API_TOKEN=bundle-only-authentik-token\n`, { encoding: "utf8", mode: 0o600, }); chmodSync(file, 0o600); chmodSync(bundle, 0o600); const original = process.env.THT_OIDC_CLIENT_SECRET; delete process.env.THT_OIDC_CLIENT_SECRET; const oidcProtocolFactory = vi.fn((input: { clientSecret: string }) => ({ authorizationUrl: async ({ state }: { state: string }) => new URL(`https://authentik.example.org/authorize?state=${state}`), callback: async () => { throw new Error("callback is outside this login-start regression"); }, diagnose: async () => undefined, })); const authSessionStore = { createOidcState: async () => ({ state: "s".repeat(43), record: { version: 1 }, }), }; try { const app = buildApp(loadConfig({ NODE_ENV: "test", THT_AUTH_CONFIG_FILE: file, THT_AUTH_STATE_ROOT: join(directory, "auth-state"), THT_SECRETS_FILE: bundle, }), { oidcProtocolFactory, authSessionStore } as never); try { const response = await app.inject({ method: "GET", url: "/auth/oidc/login" }); expect(response.statusCode).toBe(302); expect(oidcProtocolFactory).toHaveBeenCalledWith(expect.objectContaining({ clientSecret })); expect(process.env.THT_OIDC_CLIENT_SECRET).toBeUndefined(); } finally { await app.close(); } } finally { if (original === undefined) delete process.env.THT_OIDC_CLIENT_SECRET; else process.env.THT_OIDC_CLIENT_SECRET = original; rmSync(directory, { recursive: true, force: true }); } });