Compare commits
	
		
			1 Commits
		
	
	
		
			manualdeps
			...
			fix-shared
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 4f98c21f42 | 
							
								
								
									
										52
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										52
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							@ -25,7 +25,6 @@
 | 
			
		||||
                "@formatjs/intl-listformat": "^7.5.7",
 | 
			
		||||
                "@fortawesome/fontawesome-free": "^6.6.0",
 | 
			
		||||
                "@goauthentik/api": "^2025.2.4-1745325566",
 | 
			
		||||
                "@lit-labs/ssr": "^3.2.2",
 | 
			
		||||
                "@lit/context": "^1.1.2",
 | 
			
		||||
                "@lit/localize": "^0.12.2",
 | 
			
		||||
                "@lit/reactive-element": "^2.0.4",
 | 
			
		||||
@ -66,6 +65,7 @@
 | 
			
		||||
                "remark-gfm": "^4.0.1",
 | 
			
		||||
                "remark-mdx-frontmatter": "^5.0.0",
 | 
			
		||||
                "style-mod": "^4.1.2",
 | 
			
		||||
                "trusted-types": "^2.0.0",
 | 
			
		||||
                "ts-pattern": "^5.4.0",
 | 
			
		||||
                "unist-util-visit": "^5.0.0",
 | 
			
		||||
                "webcomponent-qr-code": "^1.2.0",
 | 
			
		||||
@ -2281,47 +2281,11 @@
 | 
			
		||||
                "@lezer/lr": "^1.0.0"
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "node_modules/@lit-labs/ssr": {
 | 
			
		||||
            "version": "3.3.1",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/@lit-labs/ssr/-/ssr-3.3.1.tgz",
 | 
			
		||||
            "integrity": "sha512-JlF1PempxvzrGEpRFrF+Ki0MHzR3HA51SK8Zv0cFpW9p0bPW4k0FeCwrElCu371UEpXF7RcaE2wgYaE1az0XKg==",
 | 
			
		||||
            "dependencies": {
 | 
			
		||||
                "@lit-labs/ssr-client": "^1.1.7",
 | 
			
		||||
                "@lit-labs/ssr-dom-shim": "^1.3.0",
 | 
			
		||||
                "@lit/reactive-element": "^2.0.4",
 | 
			
		||||
                "@parse5/tools": "^0.3.0",
 | 
			
		||||
                "@types/node": "^16.0.0",
 | 
			
		||||
                "enhanced-resolve": "^5.10.0",
 | 
			
		||||
                "lit": "^3.1.2",
 | 
			
		||||
                "lit-element": "^4.0.4",
 | 
			
		||||
                "lit-html": "^3.1.2",
 | 
			
		||||
                "node-fetch": "^3.2.8",
 | 
			
		||||
                "parse5": "^7.1.1"
 | 
			
		||||
            },
 | 
			
		||||
            "engines": {
 | 
			
		||||
                "node": ">=13.9.0"
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "node_modules/@lit-labs/ssr-client": {
 | 
			
		||||
            "version": "1.1.7",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/@lit-labs/ssr-client/-/ssr-client-1.1.7.tgz",
 | 
			
		||||
            "integrity": "sha512-VvqhY/iif3FHrlhkzEPsuX/7h/NqnfxLwVf0p8ghNIlKegRyRqgeaJevZ57s/u/LiFyKgqksRP5n+LmNvpxN+A==",
 | 
			
		||||
            "dependencies": {
 | 
			
		||||
                "@lit/reactive-element": "^2.0.4",
 | 
			
		||||
                "lit": "^3.1.2",
 | 
			
		||||
                "lit-html": "^3.1.2"
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "node_modules/@lit-labs/ssr-dom-shim": {
 | 
			
		||||
            "version": "1.3.0",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.3.0.tgz",
 | 
			
		||||
            "integrity": "sha512-nQIWonJ6eFAvUUrSlwyHDm/aE8PBDu5kRpL0vHMg6K8fK3Diq1xdPjTnsJSwxABhaZ+5eBi1btQB5ShUTKo4nQ=="
 | 
			
		||||
        },
 | 
			
		||||
        "node_modules/@lit-labs/ssr/node_modules/@types/node": {
 | 
			
		||||
            "version": "16.18.126",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.126.tgz",
 | 
			
		||||
            "integrity": "sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw=="
 | 
			
		||||
        },
 | 
			
		||||
        "node_modules/@lit/context": {
 | 
			
		||||
            "version": "1.1.5",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/@lit/context/-/context-1.1.5.tgz",
 | 
			
		||||
@ -3557,6 +3521,7 @@
 | 
			
		||||
            "version": "0.3.0",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/@parse5/tools/-/tools-0.3.0.tgz",
 | 
			
		||||
            "integrity": "sha512-zxRyTHkqb7WQMV8kTNBKWb1BeOFUKXBXTBWuxg9H9hfvQB3IwP6Iw2U75Ia5eyRxPNltmY7E8YAlz6zWwUnjKg==",
 | 
			
		||||
            "dev": true,
 | 
			
		||||
            "dependencies": {
 | 
			
		||||
                "parse5": "^7.0.0"
 | 
			
		||||
            }
 | 
			
		||||
@ -10723,6 +10688,7 @@
 | 
			
		||||
            "version": "4.0.1",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
 | 
			
		||||
            "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
 | 
			
		||||
            "dev": true,
 | 
			
		||||
            "engines": {
 | 
			
		||||
                "node": ">= 12"
 | 
			
		||||
            }
 | 
			
		||||
@ -11343,6 +11309,7 @@
 | 
			
		||||
            "version": "5.18.1",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
 | 
			
		||||
            "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==",
 | 
			
		||||
            "dev": true,
 | 
			
		||||
            "dependencies": {
 | 
			
		||||
                "graceful-fs": "^4.2.4",
 | 
			
		||||
                "tapable": "^2.2.0"
 | 
			
		||||
@ -13820,7 +13787,8 @@
 | 
			
		||||
        "node_modules/graceful-fs": {
 | 
			
		||||
            "version": "4.2.11",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
 | 
			
		||||
            "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
 | 
			
		||||
            "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
 | 
			
		||||
            "dev": true
 | 
			
		||||
        },
 | 
			
		||||
        "node_modules/grapheme-splitter": {
 | 
			
		||||
            "version": "1.0.4",
 | 
			
		||||
@ -18256,6 +18224,7 @@
 | 
			
		||||
            "version": "3.3.2",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
 | 
			
		||||
            "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
 | 
			
		||||
            "dev": true,
 | 
			
		||||
            "dependencies": {
 | 
			
		||||
                "data-uri-to-buffer": "^4.0.0",
 | 
			
		||||
                "fetch-blob": "^3.1.4",
 | 
			
		||||
@ -22373,6 +22342,7 @@
 | 
			
		||||
            "version": "2.2.1",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
 | 
			
		||||
            "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
 | 
			
		||||
            "dev": true,
 | 
			
		||||
            "engines": {
 | 
			
		||||
                "node": ">=6"
 | 
			
		||||
            }
 | 
			
		||||
@ -22724,6 +22694,12 @@
 | 
			
		||||
                "url": "https://github.com/sponsors/wooorm"
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "node_modules/trusted-types": {
 | 
			
		||||
            "version": "2.0.0",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/trusted-types/-/trusted-types-2.0.0.tgz",
 | 
			
		||||
            "integrity": "sha512-Eam+AUp6lg04YjmYkuLNhEJX+6ByocrKTpY/TtfRK/gV6OmxeN0OwkIasor28SUJ606snArpPLGtPMGbqdaaUA==",
 | 
			
		||||
            "license": "W3C-20150513"
 | 
			
		||||
        },
 | 
			
		||||
        "node_modules/ts-api-utils": {
 | 
			
		||||
            "version": "2.1.0",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
 | 
			
		||||
 | 
			
		||||
@ -13,7 +13,6 @@
 | 
			
		||||
        "@formatjs/intl-listformat": "^7.5.7",
 | 
			
		||||
        "@fortawesome/fontawesome-free": "^6.6.0",
 | 
			
		||||
        "@goauthentik/api": "^2025.2.4-1745325566",
 | 
			
		||||
        "@lit-labs/ssr": "^3.2.2",
 | 
			
		||||
        "@lit/context": "^1.1.2",
 | 
			
		||||
        "@lit/localize": "^0.12.2",
 | 
			
		||||
        "@lit/reactive-element": "^2.0.4",
 | 
			
		||||
@ -54,6 +53,7 @@
 | 
			
		||||
        "remark-gfm": "^4.0.1",
 | 
			
		||||
        "remark-mdx-frontmatter": "^5.0.0",
 | 
			
		||||
        "style-mod": "^4.1.2",
 | 
			
		||||
        "trusted-types": "^2.0.0",
 | 
			
		||||
        "ts-pattern": "^5.4.0",
 | 
			
		||||
        "unist-util-visit": "^5.0.0",
 | 
			
		||||
        "webcomponent-qr-code": "^1.2.0",
 | 
			
		||||
 | 
			
		||||
@ -1,26 +1,110 @@
 | 
			
		||||
import type { Config as DOMPurifyConfig } from "dompurify";
 | 
			
		||||
import DOMPurify from "dompurify";
 | 
			
		||||
import { trustedTypes } from "trusted-types";
 | 
			
		||||
 | 
			
		||||
import { render } from "@lit-labs/ssr";
 | 
			
		||||
import { collectResult } from "@lit-labs/ssr/lib/render-result.js";
 | 
			
		||||
import { TemplateResult, html } from "lit";
 | 
			
		||||
import { render } from "lit";
 | 
			
		||||
import { unsafeHTML } from "lit/directives/unsafe-html.js";
 | 
			
		||||
import { until } from "lit/directives/until.js";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Trusted types policy that escapes HTML content in place.
 | 
			
		||||
 *
 | 
			
		||||
 * @see {@linkcode SanitizedTrustPolicy} to strip HTML content.
 | 
			
		||||
 *
 | 
			
		||||
 * @returns {TrustedHTML} All HTML content, escaped.
 | 
			
		||||
 */
 | 
			
		||||
export const EscapeTrustPolicy = trustedTypes.createPolicy("authentik-escape", {
 | 
			
		||||
    createHTML: (untrustedHTML: string) => {
 | 
			
		||||
        return DOMPurify.sanitize(untrustedHTML, {
 | 
			
		||||
            RETURN_TRUSTED_TYPE: false,
 | 
			
		||||
        });
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Trusted types policy, stripping all HTML content.
 | 
			
		||||
 *
 | 
			
		||||
 * @returns {TrustedHTML} Text content only, all HTML tags stripped.
 | 
			
		||||
 */
 | 
			
		||||
export const SanitizedTrustPolicy = trustedTypes.createPolicy("authentik-sanitize", {
 | 
			
		||||
    createHTML: (untrustedHTML: string) => {
 | 
			
		||||
        return DOMPurify.sanitize(untrustedHTML, {
 | 
			
		||||
            RETURN_TRUSTED_TYPE: false,
 | 
			
		||||
            ALLOWED_TAGS: ["#text"],
 | 
			
		||||
        });
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Trusted types policy, allowing a minimal set of _safe_ HTML tags supplied by
 | 
			
		||||
 * a trusted source, such as the brand API.
 | 
			
		||||
 */
 | 
			
		||||
export const BrandedHTMLPolicy = trustedTypes.createPolicy("authentik-restrict", {
 | 
			
		||||
    createHTML: (untrustedHTML: string) => {
 | 
			
		||||
        return DOMPurify.sanitize(untrustedHTML, {
 | 
			
		||||
            RETURN_TRUSTED_TYPE: false,
 | 
			
		||||
            FORBID_TAGS: [
 | 
			
		||||
                "script",
 | 
			
		||||
                "style",
 | 
			
		||||
                "iframe",
 | 
			
		||||
                "link",
 | 
			
		||||
                "object",
 | 
			
		||||
                "embed",
 | 
			
		||||
                "applet",
 | 
			
		||||
                "meta",
 | 
			
		||||
                "base",
 | 
			
		||||
                "form",
 | 
			
		||||
                "input",
 | 
			
		||||
                "textarea",
 | 
			
		||||
                "select",
 | 
			
		||||
                "button",
 | 
			
		||||
            ],
 | 
			
		||||
            FORBID_ATTR: [
 | 
			
		||||
                "onerror",
 | 
			
		||||
                "onclick",
 | 
			
		||||
                "onload",
 | 
			
		||||
                "onmouseover",
 | 
			
		||||
                "onmouseout",
 | 
			
		||||
                "onmouseup",
 | 
			
		||||
                "onmousedown",
 | 
			
		||||
                "onfocus",
 | 
			
		||||
                "onblur",
 | 
			
		||||
                "onsubmit",
 | 
			
		||||
            ],
 | 
			
		||||
        });
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export type AuthentikTrustPolicy =
 | 
			
		||||
    | typeof EscapeTrustPolicy
 | 
			
		||||
    | typeof SanitizedTrustPolicy
 | 
			
		||||
    | typeof BrandedHTMLPolicy;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Sanitize an untrusted HTML string using a trusted types policy.
 | 
			
		||||
 */
 | 
			
		||||
export function sanitizeHTML(trustPolicy: AuthentikTrustPolicy, untrustedHTML: string) {
 | 
			
		||||
    return unsafeHTML(trustPolicy.createHTML(untrustedHTML).toString());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * DOMPurify configuration for strict sanitization.
 | 
			
		||||
 *
 | 
			
		||||
 * This configuration only allows text nodes and disallows all HTML tags.
 | 
			
		||||
 */
 | 
			
		||||
export const DOM_PURIFY_STRICT = {
 | 
			
		||||
    ALLOWED_TAGS: ["#text"],
 | 
			
		||||
} as const satisfies DOMPurifyConfig;
 | 
			
		||||
 | 
			
		||||
export async function renderStatic(input: TemplateResult): Promise<string> {
 | 
			
		||||
    return await collectResult(render(input));
 | 
			
		||||
}
 | 
			
		||||
/**
 | 
			
		||||
 * Render untrusted HTML to a string without escaping it.
 | 
			
		||||
 *
 | 
			
		||||
 * @returns {string} The rendered HTML string.
 | 
			
		||||
 */
 | 
			
		||||
export function renderStaticHTMLUnsafe(untrustedHTML: unknown): string {
 | 
			
		||||
    const container = document.createElement("html");
 | 
			
		||||
    render(untrustedHTML, container);
 | 
			
		||||
 | 
			
		||||
export function purify(input: TemplateResult): TemplateResult {
 | 
			
		||||
    return html`${until(
 | 
			
		||||
        (async () => {
 | 
			
		||||
            const rendered = await renderStatic(input);
 | 
			
		||||
            const purified = DOMPurify.sanitize(rendered);
 | 
			
		||||
            return html`${unsafeHTML(purified)}`;
 | 
			
		||||
        })(),
 | 
			
		||||
    )}`;
 | 
			
		||||
    const result = container.innerHTML;
 | 
			
		||||
 | 
			
		||||
    return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										55
									
								
								web/src/elements/utils/iframe.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								web/src/elements/utils/iframe.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,55 @@
 | 
			
		||||
/**
 | 
			
		||||
 * @file IFrame Utilities
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
interface IFrameLoadResult {
 | 
			
		||||
    contentWindow: Window;
 | 
			
		||||
    contentDocument: Document;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function pluckIFrameContent(iframe: HTMLIFrameElement) {
 | 
			
		||||
    const contentWindow = iframe.contentWindow;
 | 
			
		||||
    const contentDocument = iframe.contentDocument;
 | 
			
		||||
 | 
			
		||||
    if (!contentWindow) {
 | 
			
		||||
        throw new Error("Iframe contentWindow is not accessible");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!contentDocument) {
 | 
			
		||||
        throw new Error("Iframe contentDocument is not accessible");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        contentWindow,
 | 
			
		||||
        contentDocument,
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function resolveIFrameContent(iframe: HTMLIFrameElement): Promise<IFrameLoadResult> {
 | 
			
		||||
    if (iframe.contentDocument?.readyState === "complete") {
 | 
			
		||||
        return Promise.resolve(pluckIFrameContent(iframe));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return new Promise((resolve) => {
 | 
			
		||||
        iframe.addEventListener("load", () => resolve(pluckIFrameContent(iframe)), { once: true });
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Creates a minimal HTML wrapper for an iframe.
 | 
			
		||||
 *
 | 
			
		||||
 * @deprecated Use the `contentDocument.body` directly instead.
 | 
			
		||||
 */
 | 
			
		||||
export function createIFrameHTMLWrapper(bodyContent: string): string {
 | 
			
		||||
    const html = String.raw;
 | 
			
		||||
 | 
			
		||||
    return html`<!doctype html>
 | 
			
		||||
        <html>
 | 
			
		||||
            <head>
 | 
			
		||||
                <meta charset="utf-8" />
 | 
			
		||||
            </head>
 | 
			
		||||
            <body style="display:flex;flex-direction:row;justify-content:center;">
 | 
			
		||||
                ${bodyContent}
 | 
			
		||||
            </body>
 | 
			
		||||
        </html>`;
 | 
			
		||||
}
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import { purify } from "@goauthentik/common/purify";
 | 
			
		||||
import { BrandedHTMLPolicy, sanitizeHTML } from "@goauthentik/common/purify";
 | 
			
		||||
import { AKElement } from "@goauthentik/elements/Base.js";
 | 
			
		||||
 | 
			
		||||
import { msg } from "@lit/localize";
 | 
			
		||||
@ -21,8 +21,6 @@ const styles = css`
 | 
			
		||||
    }
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const poweredBy: FooterLink = { name: msg("Powered by authentik"), href: null };
 | 
			
		||||
 | 
			
		||||
@customElement("ak-brand-links")
 | 
			
		||||
export class BrandLinks extends AKElement {
 | 
			
		||||
    static get styles() {
 | 
			
		||||
@ -33,13 +31,21 @@ export class BrandLinks extends AKElement {
 | 
			
		||||
    links: FooterLink[] = [];
 | 
			
		||||
 | 
			
		||||
    render() {
 | 
			
		||||
        const links = [...(this.links ?? []), poweredBy];
 | 
			
		||||
        const links = [...(this.links ?? [])];
 | 
			
		||||
 | 
			
		||||
        return html` <ul class="pf-c-list pf-m-inline">
 | 
			
		||||
            ${map(links, (link) =>
 | 
			
		||||
                link.href
 | 
			
		||||
                    ? purify(html`<li><a href="${link.href}">${link.name}</a></li>`)
 | 
			
		||||
                    : html`<li><span>${link.name}</span></li>`,
 | 
			
		||||
            )}
 | 
			
		||||
            ${map(links, (link) => {
 | 
			
		||||
                const children = sanitizeHTML(BrandedHTMLPolicy, link.name);
 | 
			
		||||
 | 
			
		||||
                if (link.href) {
 | 
			
		||||
                    return html`<li><a href="${link.href}">${children}</a></li>`;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return html`<li>
 | 
			
		||||
                    <span> ${children} </span>
 | 
			
		||||
                </li>`;
 | 
			
		||||
            })}
 | 
			
		||||
            <li><span>${msg("Powered by authentik")}</span></li>
 | 
			
		||||
        </ul>`;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,15 +1,16 @@
 | 
			
		||||
///<reference types="@hcaptcha/types"/>
 | 
			
		||||
import { renderStatic } from "@goauthentik/common/purify";
 | 
			
		||||
/// <reference types="@hcaptcha/types"/>
 | 
			
		||||
/// <reference types="turnstile-types"/>
 | 
			
		||||
import { renderStaticHTMLUnsafe } from "@goauthentik/common/purify";
 | 
			
		||||
import "@goauthentik/elements/EmptyState";
 | 
			
		||||
import { akEmptyState } from "@goauthentik/elements/EmptyState";
 | 
			
		||||
import { bound } from "@goauthentik/elements/decorators/bound";
 | 
			
		||||
import "@goauthentik/elements/forms/FormElement";
 | 
			
		||||
import { createIFrameHTMLWrapper } from "@goauthentik/elements/utils/iframe";
 | 
			
		||||
import { ListenerController } from "@goauthentik/elements/utils/listenerController.js";
 | 
			
		||||
import { randomId } from "@goauthentik/elements/utils/randomId";
 | 
			
		||||
import "@goauthentik/flow/FormStatic";
 | 
			
		||||
import { BaseStage } from "@goauthentik/flow/stages/base";
 | 
			
		||||
import { P, match } from "ts-pattern";
 | 
			
		||||
import type * as _ from "turnstile-types";
 | 
			
		||||
 | 
			
		||||
import { msg } from "@lit/localize";
 | 
			
		||||
import { CSSResult, PropertyValues, TemplateResult, css, html, nothing } from "lit";
 | 
			
		||||
@ -56,40 +57,36 @@ type CaptchaHandler = {
 | 
			
		||||
// a resize. Because the Captcha is itself in an iframe, the reported height is often off by some
 | 
			
		||||
// margin, so adding 2rem of height to our container adds padding and prevents scroll bars or hidden
 | 
			
		||||
// rendering.
 | 
			
		||||
function iframeTemplate(children: TemplateResult, challengeURL: string): TemplateResult {
 | 
			
		||||
    return html` ${children}
 | 
			
		||||
        <script>
 | 
			
		||||
            new ResizeObserver((entries) => {
 | 
			
		||||
                const height =
 | 
			
		||||
                    document.body.offsetHeight +
 | 
			
		||||
                    parseFloat(getComputedStyle(document.body).fontSize) * 2;
 | 
			
		||||
 | 
			
		||||
const iframeTemplate = (captchaElement: TemplateResult, challengeUrl: string) =>
 | 
			
		||||
    html`<!doctype html>
 | 
			
		||||
        <head>
 | 
			
		||||
            <html>
 | 
			
		||||
                <body style="display:flex;flex-direction:row;justify-content:center;">
 | 
			
		||||
                    ${captchaElement}
 | 
			
		||||
                    <script>
 | 
			
		||||
                        new ResizeObserver((entries) => {
 | 
			
		||||
                            const height =
 | 
			
		||||
                                document.body.offsetHeight +
 | 
			
		||||
                                parseFloat(getComputedStyle(document.body).fontSize) * 2;
 | 
			
		||||
                            window.parent.postMessage({
 | 
			
		||||
                                message: "resize",
 | 
			
		||||
                                source: "goauthentik.io",
 | 
			
		||||
                                context: "flow-executor",
 | 
			
		||||
                                size: { height },
 | 
			
		||||
                            });
 | 
			
		||||
                        }).observe(document.querySelector(".ak-captcha-container"));
 | 
			
		||||
                    </script>
 | 
			
		||||
                    <script src=${challengeUrl}></script>
 | 
			
		||||
                    <script>
 | 
			
		||||
                        function callback(token) {
 | 
			
		||||
                            window.parent.postMessage({
 | 
			
		||||
                                message: "captcha",
 | 
			
		||||
                                source: "goauthentik.io",
 | 
			
		||||
                                context: "flow-executor",
 | 
			
		||||
                                token: token,
 | 
			
		||||
                            });
 | 
			
		||||
                        }
 | 
			
		||||
                    </script>
 | 
			
		||||
                </body>
 | 
			
		||||
            </html>
 | 
			
		||||
        </head>`;
 | 
			
		||||
                window.parent.postMessage({
 | 
			
		||||
                    message: "resize",
 | 
			
		||||
                    source: "goauthentik.io",
 | 
			
		||||
                    context: "flow-executor",
 | 
			
		||||
                    size: { height },
 | 
			
		||||
                });
 | 
			
		||||
            }).observe(document.querySelector(".ak-captcha-container"));
 | 
			
		||||
        </script>
 | 
			
		||||
 | 
			
		||||
        <script src=${challengeURL}></script>
 | 
			
		||||
 | 
			
		||||
        <script>
 | 
			
		||||
            function callback(token) {
 | 
			
		||||
                window.parent.postMessage({
 | 
			
		||||
                    message: "captcha",
 | 
			
		||||
                    source: "goauthentik.io",
 | 
			
		||||
                    context: "flow-executor",
 | 
			
		||||
                    token,
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
        </script>`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@customElement("ak-stage-captcha")
 | 
			
		||||
export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeResponseRequest> {
 | 
			
		||||
@ -305,11 +302,25 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async renderFrame(captchaElement: TemplateResult) {
 | 
			
		||||
        this.captchaFrame.contentWindow?.document.open();
 | 
			
		||||
        this.captchaFrame.contentWindow?.document.write(
 | 
			
		||||
            await renderStatic(iframeTemplate(captchaElement, this.challenge.jsUrl)),
 | 
			
		||||
        const { contentDocument } = this.captchaFrame || {};
 | 
			
		||||
 | 
			
		||||
        if (!contentDocument) {
 | 
			
		||||
            console.debug(
 | 
			
		||||
                "authentik/stages/captcha: unable to render captcha frame, no contentDocument",
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        contentDocument.open();
 | 
			
		||||
 | 
			
		||||
        contentDocument.write(
 | 
			
		||||
            createIFrameHTMLWrapper(
 | 
			
		||||
                renderStaticHTMLUnsafe(iframeTemplate(captchaElement, this.challenge.jsUrl)),
 | 
			
		||||
            ),
 | 
			
		||||
        );
 | 
			
		||||
        this.captchaFrame.contentWindow?.document.close();
 | 
			
		||||
 | 
			
		||||
        contentDocument.close();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    renderBody() {
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user