Version-control the Authentik OIDC blueprint, add full-logout + username regression tests
CI / web (push) Successful in 17s
CI / api (push) Successful in 24s

The Authentik blueprint that provisions the OAuth2 Provider/Application only
lived on CT121's filesystem via ad-hoc scp/pct push -- deploy/authentik/ is
now the source of truth, with redeploy steps in docs/oidc-setup.md.

Also documents two bugs found and fixed while implementing issue #19: the
first RP-Initiated Logout attempt only ended the app-scoped session, and the
provider had no property_mappings so the ID token's username claim was
missing (fell back to a raw sub hash that looked like a leaked session
token). Both are covered by new Playwright regression tests.

The deep-check Fingerbank test now skips instead of failing when its target
device (192.168.1.106) has since been manually labeled known via the
dashboard, rather than assuming it stays unlabeled forever.
This commit is contained in:
2026-07-13 09:19:49 -06:00
parent 74c7bf6ef7
commit 461bd63804
4 changed files with 156 additions and 12 deletions
+37 -3
View File
@@ -3,9 +3,7 @@ import { test, expect } from "@playwright/test";
const USERNAME = process.env.OIDC_USERNAME ?? "playwright-test";
const PASSWORD = process.env.OIDC_PASSWORD ?? "";
test("OIDC login via Authentik stays on public domains throughout", async ({ page }) => {
test.skip(!PASSWORD, "OIDC_PASSWORD not set");
async function loginViaAuthentik(page: import("@playwright/test").Page) {
await page.goto("/");
const oidcButton = page.getByRole("link", { name: "Sign in with Authentik" });
await expect(oidcButton).toBeVisible();
@@ -27,11 +25,47 @@ test("OIDC login via Authentik stays on public domains throughout", async ({ pag
await page.waitForURL(/^https:\/\/monitor\.jerodrigged\.com\//, { timeout: 15_000 });
expect(page.url()).not.toMatch(/192\.168\./);
}
test("OIDC login via Authentik stays on public domains throughout", async ({ page }) => {
test.skip(!PASSWORD, "OIDC_PASSWORD not set");
await loginViaAuthentik(page);
await expect(page.locator(".host-card").first()).toBeVisible({ timeout: 15_000 });
// Regression check: the OAuth2Provider originally had no property_mappings
// attached, so the ID token never carried preferred_username/email -- the
// app's claim fallback landed on the raw `sub` value, which (with
// sub_mode: hashed_user_id) is a long hash that looked like a leaked
// session token sitting next to the sign-out button.
const displayedUsername = await page.locator(".username").textContent();
expect(displayedUsername).toBe(USERNAME);
expect(displayedUsername!.length).toBeLessThan(40);
});
test("sign out ends the Authentik session too, not just the local one", async ({ page }) => {
test.skip(!PASSWORD, "OIDC_PASSWORD not set");
await loginViaAuthentik(page);
await expect(page.locator(".host-card").first()).toBeVisible({ timeout: 15_000 });
// Regression check for issue #19. First implementation redirected through
// Authentik's end_session_endpoint but used the app-scoped invalidation
// flow, which only ends this app's session and leaves Authentik's own
// browser cookie valid -- a subsequent "Sign in with Authentik" click
// would silently re-authenticate with no prompt at all. Full-logout flow
// (see docs/oidc-setup.md) is required to actually satisfy "sign out
// should sign out of Authentik too".
await page.getByRole("button", { name: "Sign out" }).click();
await page.waitForURL(/^https:\/\/auth\.jerodrigged\.com\//, { timeout: 10_000 });
await page.getByRole("link", { name: "Log back into Homelab Monitor" }).click();
await page.waitForURL(/^https:\/\/monitor\.jerodrigged\.com\//, { timeout: 10_000 });
await expect(page.getByPlaceholder("Username")).toBeVisible();
// The real assertion: a fresh "Sign in with Authentik" click must require
// actual credentials again, not silently re-authenticate.
await page.getByRole("link", { name: "Sign in with Authentik" }).click();
await page.waitForURL(/^https:\/\/auth\.jerodrigged\.com\//, { timeout: 10_000 });
await expect(page.getByPlaceholder(/email or username/i)).toBeVisible({ timeout: 10_000 });
await expect(page.locator(".device-table")).toHaveCount(0);
});
test("wrong Authentik password does not reach the dashboard", async ({ page }) => {