91 lines
4.3 KiB
TypeScript
91 lines
4.3 KiB
TypeScript
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 });
|
|
}
|
|
});
|