import { expect, test, type Page } from "@playwright/test"; import { createAuthenticationStack } from "./fixtures/auth-stack.mjs"; test.describe.configure({ mode: "serial" }); // The only HTTPS navigation in this file is the test-scoped loopback provider. test.use({ ignoreHTTPSErrors: true }); let stack: Awaited>; test.beforeAll(async () => { stack = await createAuthenticationStack(); }); test.afterAll(async () => { await stack?.close(); }); test("the loopback fixture exposes signed OIDC discovery, device authorization, and AuthentiK group lookup", async () => { await expect(stack.providerSurface()).resolves.toEqual({ discovery: true, jwks: true, deviceAuthorization: true, deviceToken: true, groupList: true, runtimeCredential: process.env.THT_TASK15_SENTINEL !== undefined, }); }); 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 { await expect(page.getByTestId("app-shell")).toBeVisible({ timeout: 30_000 }); } async function signInLocally(page: Page, account: "ordinary" | "admin", remember = false): Promise { await page.getByLabel("Username").fill(stack.localAccount(account).username); await page.getByLabel("Password", { exact: true }).fill(stack.localAccount(account).password); const rememberControl = page.getByRole("checkbox", { name: /remember me/i }); if (remember) await rememberControl.check(); await page.getByRole("button", { name: "Sign in", exact: true }).click(); await expectShell(page); } async function browserSession(page: Page): Promise<{ status: number; body: Record }> { return page.evaluate(async () => { const response = await fetch("/api/me", { credentials: "same-origin" }); return { status: response.status, body: await response.json() as Record }; }); } async function expectNoWebStorageTokens(page: Page): Promise { const entries = await page.evaluate(() => { const values = (storage: Storage) => Array.from({ length: storage.length }, (_unused, index) => { const key = storage.key(index) ?? ""; return [key, storage.getItem(key) ?? ""]; }); return [...values(localStorage), ...values(sessionStorage)]; }); expect(entries.filter(([key, value]) => /(?:access|refresh|id)?[_-]?token|bearer|jwt/i.test(`${key}\n${value}`))).toEqual([]); } async function signInWithOidc(page: Page): Promise { await page.getByRole("button", { name: /continue with single sign-on/i }).click(); } async function expectOidcCallbackDenied(page: Page): Promise { await expect(page.locator("body")).toContainText("OIDC sign-in could not be completed", { timeout: 30_000 }); await expect(page.getByTestId("app-shell")).toHaveCount(0); await expectNoWebStorageTokens(page); } test("local ordinary and remembered sessions survive restart, logout, and keep tokens out of Web Storage", async ({ page }) => { await stack.useLocalMode(); await page.goto(stack.publicUrl); await expect(page.getByRole("heading", { name: "Sign in to ThothII" })).toBeVisible(); await signInLocally(page, "ordinary"); expect((await browserSession(page)).body.roles).toEqual(["user"]); await expectNoWebStorageTokens(page); await stack.restartBackend(); await page.reload(); await expectShell(page); await page.getByRole("button", { name: "Log out", exact: true }).click(); await expect(page.getByRole("heading", { name: "Sign in to ThothII" })).toBeVisible(); await signInLocally(page, "ordinary", true); const remembered = (await page.context().cookies(stack.publicUrl)).find((cookie) => cookie.name === "thothii_session"); expect(remembered?.httpOnly).toBe(true); expect(remembered?.expires ?? -1).toBeGreaterThan(Date.now() / 1_000); await stack.restartBackend(); await page.reload(); await expectShell(page); await expectNoWebStorageTokens(page); await page.getByRole("button", { name: "Log out", exact: true }).click(); await expect(page.getByRole("heading", { name: "Sign in to ThothII" })).toBeVisible(); expect((await page.context().cookies(stack.publicUrl)).some((cookie) => cookie.name === "thothii_session")).toBe(false); }); test("local administrator receives the administrator role", async ({ page }) => { await stack.useLocalMode(); await page.goto(stack.publicUrl); await signInLocally(page, "admin"); expect((await browserSession(page)).body.roles).toEqual(["admin"]); await expectNoWebStorageTokens(page); }); test("OIDC Authorization Code plus PKCE redirects back and maps ordinary and administrator groups", async ({ page }) => { await stack.useOidcMode("ordinary"); await page.goto(stack.publicUrl); await signInWithOidc(page); await expectShell(page); expect((await browserSession(page)).body.roles).toEqual(["user"]); expect(stack.lastAuthorization()).toMatchObject({ codeChallengeMethod: "S256", pkceVerified: true }); await expectNoWebStorageTokens(page); await expect(page.getByRole("button", { name: "Log out", exact: true })).toHaveCount(0); await page.context().clearCookies(); stack.setOidcIdentity("admin"); await page.goto(stack.publicUrl); await signInWithOidc(page); await expectShell(page); expect((await browserSession(page)).body.roles).toEqual(["admin"]); await expectNoWebStorageTokens(page); }); test("OIDC unmapped, missing, and malformed groups fail closed; an expired token can recover", async ({ page }) => { await stack.useOidcMode("unmapped"); await page.goto(stack.publicUrl); await signInWithOidc(page); await expect(page.getByRole("heading", { name: "Access not permitted" })).toBeVisible({ timeout: 30_000 }); await expectNoWebStorageTokens(page); await page.context().clearCookies(); stack.setOidcIdentity("missing-groups"); await page.goto(stack.publicUrl); await signInWithOidc(page); await expectOidcCallbackDenied(page); stack.setOidcIdentity("malformed-groups"); await page.goto(stack.publicUrl); await signInWithOidc(page); await expectOidcCallbackDenied(page); stack.setOidcIdentity("expired"); await page.goto(stack.publicUrl); await signInWithOidc(page); await expectOidcCallbackDenied(page); stack.setOidcIdentity("ordinary"); await page.goto(stack.publicUrl); await signInWithOidc(page); await expectShell(page); expect((await browserSession(page)).body.roles).toEqual(["user"]); await expectNoWebStorageTokens(page); });