test(auth): harden Task 15 OIDC smoke evidence
This commit is contained in:
@@ -25,6 +25,33 @@ test("the loopback fixture exposes signed OIDC discovery, device authorization,
|
||||
});
|
||||
});
|
||||
|
||||
test("the production OIDC client rejects a wrong registration and wrong mounted client secret", async ({ page }) => {
|
||||
await stack.useOidcMode("ordinary", "wrong-client-id");
|
||||
await page.goto(stack.publicUrl);
|
||||
await signInWithOidc(page);
|
||||
await expect(page.locator("body")).toContainText("invalid_request");
|
||||
await expect(page.getByTestId("app-shell")).toHaveCount(0);
|
||||
|
||||
await stack.useOidcMode("ordinary", "wrong-client-secret");
|
||||
await page.goto(stack.publicUrl);
|
||||
await signInWithOidc(page);
|
||||
await expectOidcCallbackDenied(page);
|
||||
});
|
||||
|
||||
test("production diagnostics reject a wrong group token and pass with the correct mounted secrets", async () => {
|
||||
await stack.useOidcMode("ordinary", "wrong-api-token");
|
||||
await expect(stack.authDiagnostics()).resolves.toMatchObject({
|
||||
status: 1,
|
||||
report: { ready: false, checks: [{ code: "oidc_group_catalog_unauthorized" }] },
|
||||
});
|
||||
|
||||
await stack.useOidcMode("ordinary", "correct");
|
||||
await expect(stack.authDiagnostics()).resolves.toMatchObject({
|
||||
status: 0,
|
||||
report: { ready: true, checks: [{ code: "auth_ready" }] },
|
||||
});
|
||||
});
|
||||
|
||||
async function expectShell(page: Page): Promise<void> {
|
||||
await expect(page.getByTestId("app-shell")).toBeVisible({ timeout: 30_000 });
|
||||
}
|
||||
|
||||
@@ -17,6 +17,15 @@ const harnessRoot = join(repositoryRoot, "harness");
|
||||
const thtRoot = join(repositoryRoot, "tools", "tht");
|
||||
const fakePi = join(frontendRoot, "e2e", "fixtures", "fake-pi.mjs");
|
||||
const fakeTht = join(frontendRoot, "e2e", "fixtures", "fake-tht.mjs");
|
||||
const FIXTURE_CLIENT_ID = "thothii-e2e-client";
|
||||
const FIXTURE_CLIENT_SECRET = "e2e-client-secret-not-a-production-secret";
|
||||
const FIXTURE_API_TOKEN = "e2e-group-catalog-token-not-a-production-secret";
|
||||
const WRONG_CLIENT_ID = "thothii-e2e-unregistered-client";
|
||||
const WRONG_CLIENT_SECRET = "e2e-wrong-client-secret-not-production";
|
||||
const WRONG_API_TOKEN = "e2e-wrong-api-token-not-production";
|
||||
const OIDC_CREDENTIAL_VARIANTS = new Set([
|
||||
"correct", "wrong-client-id", "wrong-client-secret", "wrong-api-token",
|
||||
]);
|
||||
|
||||
function safeError(code) {
|
||||
return new Error(code);
|
||||
@@ -247,6 +256,48 @@ function managedProcess(command, args, options) {
|
||||
};
|
||||
}
|
||||
|
||||
function oneShotJson(command, args, options) {
|
||||
return new Promise((resolveCommand) => {
|
||||
const child = spawn(command, args, {
|
||||
cwd: options.cwd,
|
||||
env: options.env,
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
});
|
||||
let output = "";
|
||||
let outputValid = true;
|
||||
let settled = false;
|
||||
const finish = (status) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
clearTimeout(timer);
|
||||
let report;
|
||||
try {
|
||||
report = outputValid ? JSON.parse(output) : undefined;
|
||||
} catch {
|
||||
report = undefined;
|
||||
}
|
||||
resolveCommand({
|
||||
status: Number.isInteger(status) ? status : 1,
|
||||
report: report && typeof report === "object"
|
||||
? report
|
||||
: { ready: false, mode: "none", checks: [{ code: "fixture_command_invalid" }] },
|
||||
});
|
||||
};
|
||||
child.stdout?.on("data", (chunk) => {
|
||||
if (!outputValid) return;
|
||||
output += String(chunk);
|
||||
if (Buffer.byteLength(output) > 64 * 1024) {
|
||||
output = "";
|
||||
outputValid = false;
|
||||
child.kill("SIGKILL");
|
||||
}
|
||||
});
|
||||
child.once("error", () => finish(1));
|
||||
child.once("exit", (status) => finish(status));
|
||||
const timer = setTimeout(() => child.kill("SIGKILL"), 35_000);
|
||||
});
|
||||
}
|
||||
|
||||
async function waitForOk(url, processHandle) {
|
||||
for (let attempt = 0; attempt < 300; attempt += 1) {
|
||||
if (processHandle.exited()) {
|
||||
@@ -303,7 +354,15 @@ export async function createAuthenticationStack({ withF1Workspace = false } = {}
|
||||
const publicUrl = `http://127.0.0.1:${frontendPort}`;
|
||||
const backendUrl = `http://127.0.0.1:${backendPort}`;
|
||||
const workspace = withF1Workspace ? await prepareF1Workspace(root) : undefined;
|
||||
const provider = await startFakeOidcProvider({ directory: providerRoot });
|
||||
const provider = await startFakeOidcProvider({
|
||||
directory: providerRoot,
|
||||
registration: {
|
||||
clientId: FIXTURE_CLIENT_ID,
|
||||
clientSecret: FIXTURE_CLIENT_SECRET,
|
||||
redirectUri: new URL("/api/auth/oidc/callback", publicUrl).href,
|
||||
},
|
||||
apiToken: FIXTURE_API_TOKEN,
|
||||
});
|
||||
await buildAuthenticationStorageBridge(authStorageBinary);
|
||||
const localPassword = "e2e-local-password";
|
||||
const passwordHash = await testPasswordHash(localPassword);
|
||||
@@ -334,11 +393,17 @@ export async function createAuthenticationStack({ withF1Workspace = false } = {}
|
||||
},
|
||||
],
|
||||
}));
|
||||
writeSecure(secretsFile, [
|
||||
"THT_OIDC_CLIENT_SECRET=e2e-client-secret-not-a-production-secret",
|
||||
"THT_AUTHENTIK_API_TOKEN=e2e-group-catalog-token-not-a-production-secret",
|
||||
"",
|
||||
].join("\n"));
|
||||
function writeOidcSecrets(variant) {
|
||||
if (!OIDC_CREDENTIAL_VARIANTS.has(variant)) throw safeError("e2e_oidc_credential_variant_invalid");
|
||||
const selectedClientSecret = variant === "wrong-client-secret" ? WRONG_CLIENT_SECRET : FIXTURE_CLIENT_SECRET;
|
||||
const selectedApiToken = variant === "wrong-api-token" ? WRONG_API_TOKEN : FIXTURE_API_TOKEN;
|
||||
writeSecure(secretsFile, [
|
||||
`THT_OIDC_CLIENT_SECRET=${selectedClientSecret}`,
|
||||
`THT_AUTHENTIK_API_TOKEN=${selectedApiToken}`,
|
||||
"",
|
||||
].join("\n"));
|
||||
}
|
||||
writeOidcSecrets("correct");
|
||||
if (workspace) {
|
||||
writeSecure(fixtureDwhPasswordFile, "fixture-password-not-a-secret\n");
|
||||
writeSecure(fixtureDwhCaFile, "fixture-ca-not-a-production-certificate\n");
|
||||
@@ -378,7 +443,7 @@ export async function createAuthenticationStack({ withF1Workspace = false } = {}
|
||||
},
|
||||
local: { usersFile: "users.yaml" },
|
||||
});
|
||||
const oidcConfig = () => ({
|
||||
const oidcConfig = (variant = "correct") => ({
|
||||
version: 1,
|
||||
mode: "oidc",
|
||||
publicUrl,
|
||||
@@ -389,7 +454,7 @@ export async function createAuthenticationStack({ withF1Workspace = false } = {}
|
||||
},
|
||||
oidc: {
|
||||
issuer: provider.issuer,
|
||||
clientId: "thothii-e2e-client",
|
||||
clientId: variant === "wrong-client-id" ? WRONG_CLIENT_ID : FIXTURE_CLIENT_ID,
|
||||
clientSecretRef: "THT_OIDC_CLIENT_SECRET",
|
||||
scopes: ["openid", "profile", "groups"],
|
||||
groupsClaim: "groups",
|
||||
@@ -407,42 +472,46 @@ export async function createAuthenticationStack({ withF1Workspace = false } = {}
|
||||
},
|
||||
});
|
||||
|
||||
function backendEnvironment() {
|
||||
return cleanBackendEnvironment({
|
||||
NODE_ENV: "test",
|
||||
HOST: "127.0.0.1",
|
||||
PORT: String(backendPort),
|
||||
PI_BIN: fakePi,
|
||||
THT_BIN: fakeTht,
|
||||
THT_AUTH_STORAGE_BIN: authStorageBinary,
|
||||
THT_HARNESS_DIR: harnessRoot,
|
||||
THT_AUTH_CONFIG_FILE: authConfigFile,
|
||||
THT_AUTH_STATE_ROOT: stateRoot,
|
||||
THT_SECRETS_FILE: secretsFile,
|
||||
NODE_EXTRA_CA_CERTS: provider.caFile,
|
||||
SETTINGS_FILE: settingsFile,
|
||||
THT_MAINTENANCE_FILE: maintenanceFile,
|
||||
THT_WORKSPACE_REGISTRY_ROOT: registryRoot,
|
||||
THT_WORKSPACE_SECRET_STORE_ROOT: workspaceSecretRoot,
|
||||
THT_WORKSPACE_SECRET_RUNTIME_ROOT: workspaceRuntimeRoot,
|
||||
THT_WORKSPACE_INSTALLATION_ID: "e2e",
|
||||
THT_DATA_ROOT: join(root, "data"),
|
||||
...(qdrant ? { THT_INTERNAL_QDRANT_URL: qdrant.baseUrl } : {}),
|
||||
...(workspace ? {
|
||||
THT_WORKSPACE_GIT_REMOTE: workspace.remote,
|
||||
THT_WORKSPACE_GIT_BRANCH: "main",
|
||||
THT_WORKSPACE_SECRET_ROOTS: fixtureSecretRoot,
|
||||
THT_WS_FIXTURE_WORKSPACE_DWH_TRANSPORT: "postgres_direct",
|
||||
THT_WS_FIXTURE_WORKSPACE_DWH_HOST: "127.0.0.1",
|
||||
THT_WS_FIXTURE_WORKSPACE_DWH_PORT: "5432",
|
||||
THT_WS_FIXTURE_WORKSPACE_DWH_USER: "fixture",
|
||||
THT_WS_FIXTURE_WORKSPACE_DWH_PASSWORD_FILE: fixtureDwhPasswordFile,
|
||||
THT_WS_FIXTURE_WORKSPACE_DWH_TLS_CA_FILE: fixtureDwhCaFile,
|
||||
} : {}),
|
||||
});
|
||||
}
|
||||
|
||||
async function startBackend() {
|
||||
if (mode === undefined) throw safeError("e2e_auth_mode_not_configured");
|
||||
backend = managedProcess(join(backendRoot, "node_modules", ".bin", "tsx"), ["src/server.ts"], {
|
||||
cwd: backendRoot,
|
||||
env: cleanBackendEnvironment({
|
||||
NODE_ENV: "test",
|
||||
HOST: "127.0.0.1",
|
||||
PORT: String(backendPort),
|
||||
PI_BIN: fakePi,
|
||||
THT_BIN: fakeTht,
|
||||
THT_AUTH_STORAGE_BIN: authStorageBinary,
|
||||
THT_HARNESS_DIR: harnessRoot,
|
||||
THT_AUTH_CONFIG_FILE: authConfigFile,
|
||||
THT_AUTH_STATE_ROOT: stateRoot,
|
||||
THT_SECRETS_FILE: secretsFile,
|
||||
NODE_EXTRA_CA_CERTS: provider.caFile,
|
||||
SETTINGS_FILE: settingsFile,
|
||||
THT_MAINTENANCE_FILE: maintenanceFile,
|
||||
THT_WORKSPACE_REGISTRY_ROOT: registryRoot,
|
||||
THT_WORKSPACE_SECRET_STORE_ROOT: workspaceSecretRoot,
|
||||
THT_WORKSPACE_SECRET_RUNTIME_ROOT: workspaceRuntimeRoot,
|
||||
THT_WORKSPACE_INSTALLATION_ID: "e2e",
|
||||
THT_DATA_ROOT: join(root, "data"),
|
||||
...(qdrant ? { THT_INTERNAL_QDRANT_URL: qdrant.baseUrl } : {}),
|
||||
...(workspace ? {
|
||||
THT_WORKSPACE_GIT_REMOTE: workspace.remote,
|
||||
THT_WORKSPACE_GIT_BRANCH: "main",
|
||||
THT_WORKSPACE_SECRET_ROOTS: fixtureSecretRoot,
|
||||
THT_WS_FIXTURE_WORKSPACE_DWH_TRANSPORT: "postgres_direct",
|
||||
THT_WS_FIXTURE_WORKSPACE_DWH_HOST: "127.0.0.1",
|
||||
THT_WS_FIXTURE_WORKSPACE_DWH_PORT: "5432",
|
||||
THT_WS_FIXTURE_WORKSPACE_DWH_USER: "fixture",
|
||||
THT_WS_FIXTURE_WORKSPACE_DWH_PASSWORD_FILE: fixtureDwhPasswordFile,
|
||||
THT_WS_FIXTURE_WORKSPACE_DWH_TLS_CA_FILE: fixtureDwhCaFile,
|
||||
} : {}),
|
||||
}),
|
||||
env: backendEnvironment(),
|
||||
});
|
||||
await waitForOk(`${backendUrl}/health`, backend);
|
||||
}
|
||||
@@ -468,15 +537,23 @@ export async function createAuthenticationStack({ withF1Workspace = false } = {}
|
||||
const jwks = await providerJson(`${provider.issuer}jwks`, provider.caFile);
|
||||
const device = await providerJson(`${provider.issuer}device_authorization`, provider.caFile, {
|
||||
method: "POST",
|
||||
body: "client_id=thothii-e2e-client",
|
||||
body: new URLSearchParams({
|
||||
client_id: FIXTURE_CLIENT_ID,
|
||||
client_secret: FIXTURE_CLIENT_SECRET,
|
||||
}).toString(),
|
||||
});
|
||||
const deviceCode = typeof device.body?.device_code === "string" ? device.body.device_code : "";
|
||||
const deviceToken = deviceCode.length === 0 ? { status: 0, body: {} } : await providerJson(`${provider.issuer}token`, provider.caFile, {
|
||||
method: "POST",
|
||||
body: `grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code&device_code=${encodeURIComponent(deviceCode)}`,
|
||||
body: new URLSearchParams({
|
||||
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
||||
client_id: FIXTURE_CLIENT_ID,
|
||||
client_secret: FIXTURE_CLIENT_SECRET,
|
||||
device_code: deviceCode,
|
||||
}).toString(),
|
||||
});
|
||||
const groups = await providerJson(`${provider.baseUrl}/api/v3/core/groups/?name=fixture-users`, provider.caFile, {
|
||||
headers: { authorization: "Bearer e2e-fixture" },
|
||||
headers: { authorization: `Bearer ${FIXTURE_API_TOKEN}` },
|
||||
});
|
||||
return {
|
||||
discovery: discovery.status === 200 && discovery.body?.issuer === provider.issuer,
|
||||
@@ -495,12 +572,22 @@ export async function createAuthenticationStack({ withF1Workspace = false } = {}
|
||||
mode = "local";
|
||||
await restartBackend();
|
||||
},
|
||||
async useOidcMode(identity) {
|
||||
async useOidcMode(identity, variant = "correct") {
|
||||
if (!OIDC_CREDENTIAL_VARIANTS.has(variant)) throw safeError("e2e_oidc_credential_variant_invalid");
|
||||
provider.setIdentity(identity);
|
||||
writeSecure(authConfigFile, JSON.stringify(oidcConfig()));
|
||||
writeOidcSecrets(variant);
|
||||
writeSecure(authConfigFile, JSON.stringify(oidcConfig(variant)));
|
||||
mode = "oidc";
|
||||
await restartBackend();
|
||||
},
|
||||
async authDiagnostics() {
|
||||
if (mode !== "oidc") throw safeError("e2e_oidc_mode_required");
|
||||
return oneShotJson(
|
||||
join(backendRoot, "node_modules", ".bin", "tsx"),
|
||||
["src/auth/diagnostic-command.ts", "--json"],
|
||||
{ cwd: backendRoot, env: backendEnvironment() },
|
||||
);
|
||||
},
|
||||
restartBackend,
|
||||
async close() {
|
||||
await backend?.close();
|
||||
|
||||
Reference in New Issue
Block a user