Version-control the Authentik OIDC blueprint, add full-logout + username regression tests
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:
@@ -44,12 +44,15 @@ test("deep check surfaces a Fingerbank identification with a confidence label",
|
||||
await expect(page.locator(".device-table tbody tr").first()).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
// The Nintendo device (192.168.1.106) reliably gets a Fingerbank match, if
|
||||
// it's still on the network -- skip rather than fail if it's gone, since
|
||||
// this specific IP isn't something the test suite controls.
|
||||
// it's still on the network and still unknown -- skip rather than fail if
|
||||
// it's gone, or if it's since been manually labeled via the dashboard (its
|
||||
// "known" state isn't something the test suite controls either).
|
||||
const row = page.locator('tr[data-ip="192.168.1.106"]');
|
||||
const deepCheckButton = row.getByRole("button", { name: /deep check/i });
|
||||
test.skip((await row.count()) === 0, "192.168.1.106 not currently on the network");
|
||||
test.skip((await deepCheckButton.count()) === 0, "192.168.1.106 has since been labeled known");
|
||||
|
||||
await row.getByRole("button", { name: /deep check/i }).click();
|
||||
await deepCheckButton.click();
|
||||
await expect(page.locator(".deep-check-row").first()).toBeVisible({ timeout: 20_000 });
|
||||
|
||||
const resultText = await page.locator(".deep-check-row").first().textContent();
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
Reference in New Issue
Block a user