Some checks are pending
E2E / Playwright e2e (push) Waiting to run
- Updated login and signup components to utilize AuthCard for better user experience during redirection. - Introduced AuthentikEmbedDialog for seamless integration of Authentik's identity portal within the application. - Enhanced password recovery and signup flows with dynamic theme handling and improved loading states. - Refactored existing components to streamline authentication processes and improve maintainability.
170 lines
5.5 KiB
JavaScript
170 lines
5.5 KiB
JavaScript
// Headless dump of an AUTHENTICATED Authentik page (user-settings SPA, etc.).
|
|
//
|
|
// The user-settings interface (/auth/if/user/) redirects to login when
|
|
// unauthenticated, so a plain dump is useless. This variant logs in first.
|
|
//
|
|
// Two auth modes:
|
|
// 1) Recovery link (no password needed). Generate one on the host with:
|
|
// docker exec deploy-authentik-server-1 ak create_recovery_key 1 akadmin
|
|
// then pass the returned "/auth/recovery/use-token/<token>/" path.
|
|
// 2) Identification + password (set AK_USER / AK_PASS env vars).
|
|
//
|
|
// Usage:
|
|
// AK_RECOVERY="/auth/recovery/use-token/XXer/" \
|
|
// node scripts/authentik-dom-dump-auth.mjs "<targetUrl>" "<outName>" "<light|dark>"
|
|
// # or
|
|
// AK_USER=akadmin AK_PASS=... \
|
|
// node scripts/authentik-dom-dump-auth.mjs "<targetUrl>" "<outName>" "<light|dark>"
|
|
import { chromium } from "@playwright/test"
|
|
import { writeFileSync } from "node:fs"
|
|
|
|
const target = process.argv[2] ?? "http://localhost/auth/if/user/#/settings"
|
|
const outName = process.argv[3] ?? "user-settings"
|
|
const theme = process.argv[4] ?? "light"
|
|
const origin = process.env.AK_ORIGIN ?? "http://localhost"
|
|
|
|
const recovery = process.env.AK_RECOVERY ?? ""
|
|
const akUser = process.env.AK_USER ?? "akadmin"
|
|
const akPass = process.env.AK_PASS ?? ""
|
|
|
|
const outDir = "/tmp/authentik-dom"
|
|
await import("node:fs").then((fs) => fs.mkdirSync(outDir, { recursive: true }))
|
|
|
|
const browser = await chromium.launch()
|
|
const ctx = await browser.newContext({
|
|
viewport: { width: 1280, height: 900 },
|
|
colorScheme: theme === "dark" ? "dark" : "light",
|
|
ignoreHTTPSErrors: true,
|
|
})
|
|
const page = await ctx.newPage()
|
|
|
|
// Authentik advertises api.base as https://localhost, but nginx is http-only here.
|
|
await page.addInitScript(() => {
|
|
Object.defineProperty(window, "authentik", {
|
|
configurable: true,
|
|
set(v) {
|
|
try {
|
|
if (v && v.api) {
|
|
v.api.base = location.origin + "/auth/"
|
|
v.api.relBase = "/auth/"
|
|
}
|
|
} catch {}
|
|
Object.defineProperty(window, "authentik", {
|
|
value: v,
|
|
writable: true,
|
|
configurable: true,
|
|
})
|
|
},
|
|
get() {
|
|
return undefined
|
|
},
|
|
})
|
|
})
|
|
|
|
async function loginWithRecovery() {
|
|
const url = recovery.startsWith("http") ? recovery : origin + recovery
|
|
await page.goto(url, { waitUntil: "networkidle", timeout: 30000 })
|
|
// Recovery flow may present a "use this token" confirmation or land straight
|
|
// on the user interface. Give it time and click any primary continue button.
|
|
await page.waitForTimeout(2500)
|
|
for (let i = 0; i < 4; i++) {
|
|
const clicked = await page.evaluate(() => {
|
|
function findBtn(root) {
|
|
const btn = root.querySelector?.("button.pf-m-primary, button[type=submit]")
|
|
if (btn) return btn
|
|
const all = root.querySelectorAll?.("*") ?? []
|
|
for (const el of all) {
|
|
if (el.shadowRoot) {
|
|
const f = findBtn(el.shadowRoot)
|
|
if (f) return f
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
const b = findBtn(document)
|
|
if (b) {
|
|
b.click()
|
|
return true
|
|
}
|
|
return false
|
|
})
|
|
if (!clicked) break
|
|
await page.waitForTimeout(1500)
|
|
}
|
|
}
|
|
|
|
async function loginWithPassword() {
|
|
await page.goto(`${origin}/auth/if/flow/default-authentication-flow/`, {
|
|
waitUntil: "networkidle",
|
|
timeout: 30000,
|
|
})
|
|
await page.waitForTimeout(2000)
|
|
// Fill identification, submit, then password, submit — reaching across shadow roots.
|
|
const typeAndSubmit = async (value) => {
|
|
await page.evaluate((val) => {
|
|
function findInput(root) {
|
|
const i = root.querySelector?.("input[name=uidField], input[name=password], input")
|
|
if (i) return i
|
|
for (const el of root.querySelectorAll?.("*") ?? []) {
|
|
if (el.shadowRoot) {
|
|
const f = findInput(el.shadowRoot)
|
|
if (f) return f
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
const input = findInput(document)
|
|
if (input) {
|
|
input.value = val
|
|
input.dispatchEvent(new Event("input", { bubbles: true }))
|
|
}
|
|
}, value)
|
|
await page.keyboard.press("Enter")
|
|
await page.waitForTimeout(2500)
|
|
}
|
|
await typeAndSubmit(akUser)
|
|
await typeAndSubmit(akPass)
|
|
}
|
|
|
|
if (recovery) {
|
|
await loginWithRecovery()
|
|
} else {
|
|
await loginWithPassword()
|
|
}
|
|
|
|
// Now navigate to the authenticated target.
|
|
await page.goto(target, { waitUntil: "networkidle", timeout: 30000 })
|
|
await page.waitForTimeout(3500)
|
|
|
|
// Recursively serialize light + shadow DOM into an indented outline.
|
|
const outline = await page.evaluate(() => {
|
|
function attrs(el) {
|
|
return [...el.attributes]
|
|
.filter((a) => !["style"].includes(a.name))
|
|
.map((a) => (a.value ? `${a.name}="${a.value}"` : a.name))
|
|
.join(" ")
|
|
}
|
|
function walk(node, depth, lines) {
|
|
const pad = " ".repeat(depth)
|
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
const tag = node.tagName.toLowerCase()
|
|
const a = attrs(node)
|
|
lines.push(`${pad}<${tag}${a ? " " + a : ""}>`)
|
|
if (node.shadowRoot) {
|
|
lines.push(`${pad} #shadow-root`)
|
|
for (const c of node.shadowRoot.children) walk(c, depth + 2, lines)
|
|
}
|
|
for (const c of node.children) walk(c, depth + 1, lines)
|
|
}
|
|
return lines
|
|
}
|
|
const lines = []
|
|
walk(document.documentElement, 0, lines)
|
|
return lines.slice(0, 2000).join("\n")
|
|
})
|
|
|
|
writeFileSync(`${outDir}/${outName}-${theme}.dom.txt`, outline)
|
|
await page.screenshot({ path: `${outDir}/${outName}-${theme}.png`, fullPage: true })
|
|
console.log(`wrote ${outDir}/${outName}-${theme}.dom.txt (+ .png) — final url: ${page.url()}`)
|
|
await browser.close()
|