open-webui: SSO via authentik (OIDC authorization-code flow)

- quadlet: OAUTH_*/OPENID_* env + EnvironmentFile for the client secret
  (secret stays off-repo in /home/dank/Containers/open-webui/env, mode 600)
- authentik objects created live: application 'open-webui', OAuth2 provider
  'Open WebUI' (Strict redirect URI), 'Open WebUI roles' scope mapping,
  entitlements Open WebUI Users/Admins bound to openwebui_users/admins
- e2e: 18-openwebui-sso.spec.ts (silent SSO round-trip) + prefix in config
- unit tests: OIDC wiring + secret-hygiene assertions
- docs: SSO section + history entry
This commit is contained in:
Frank
2026-09-24 20:04:17 -07:00
parent 6ffcde0197
commit b48de913b8
5 changed files with 130 additions and 2 deletions
+3 -2
View File
@@ -28,8 +28,9 @@ export default defineConfig({
// whose prefix is not listed in any project silently never runs — a
// green suite with a test that does not exist. 17- joined on 2026-09-16
// (the hermes vhost, whose upstream is a host address rather than a
// container name, so it needs its own pass condition).
testMatch: /(10|12|13|16|17)-.*\.spec\.ts/,
// container name, so it needs its own pass condition). 18- joined on
// 2026-09-24 (OpenWebUI SSO through authentik).
testMatch: /(10|12|13|16|17|18)-.*\.spec\.ts/,
use: { storageState: ".auth/qa-e2e.json" },
dependencies: ["setup"],
},
+36
View File
@@ -0,0 +1,36 @@
import { expect, test } from "@playwright/test";
// OpenWebUI (ai.franksdank.cloud) authenticates through authentik via the
// OIDC authorization-code flow. With ONLY the standard authentik session,
// visiting the vhost must show the "Continue with authentik" button, and
// clicking it must complete the round-trip silently (no password prompt on
// the IdP) and land in the logged-in app.
test("SSO: OpenWebUI login completes through authentik silently", async ({ page }) => {
test.setTimeout(180_000);
await page.goto("https://ai.franksdank.cloud/");
// The app is a React SPA; the login page (no local session) renders the
// OAuth button labelled after the provider name.
const ssoButton = page.getByRole("button", { name: /continue with authentik/i });
await expect(ssoButton).toBeVisible({ timeout: 90_000 });
// The round-trip: ai -> auth (silent, the session is already there) ->
// back to ai with the code. No authentik password form may appear.
await ssoButton.click();
await expect
.poll(
async () => {
const body = (await page.locator("body").innerText()).toLowerCase();
return !body.includes("continue with authentik");
},
{ timeout: 90_000 },
)
.toBe(true);
await expect
.poll(() => new URL(page.url()).hostname, { timeout: 60_000 })
.toBe("ai.franksdank.cloud");
const body = await page.locator("body").innerText();
expect(body.toLowerCase()).not.toContain("powered by authentik"); // IdP flow shell
expect(body.toLowerCase()).not.toContain("password"); // no credential prompt
});