web: Fix issue where references to Lit SSR break page styles.
This commit is contained in:
		 Teffen Ellis
					Teffen Ellis
				
			
				
					committed by
					
						 Teffen Ellis
						Teffen Ellis
					
				
			
			
				
	
			
			
			 Teffen Ellis
						Teffen Ellis
					
				
			
						parent
						
							3f81bde962
						
					
				
				
					commit
					f4ed74c0c7
				
			
							
								
								
									
										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="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,18 +57,14 @@ 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. | ||||
|  | ||||
| const iframeTemplate = (captchaElement: TemplateResult, challengeUrl: string) => | ||||
|     html`<!doctype html> | ||||
|         <head> | ||||
|             <html> | ||||
|                 <body style="display:flex;flex-direction:row;justify-content:center;"> | ||||
|                     ${captchaElement} | ||||
| 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; | ||||
|  | ||||
|                 window.parent.postMessage({ | ||||
|                     message: "resize", | ||||
|                     source: "goauthentik.io", | ||||
| @ -76,20 +73,20 @@ const iframeTemplate = (captchaElement: TemplateResult, challengeUrl: string) => | ||||
|                 }); | ||||
|             }).observe(document.querySelector(".ak-captcha-container")); | ||||
|         </script> | ||||
|                     <script src=${challengeUrl}></script> | ||||
|  | ||||
|         <script src=${challengeURL}></script> | ||||
|  | ||||
|         <script> | ||||
|             function callback(token) { | ||||
|                 window.parent.postMessage({ | ||||
|                     message: "captcha", | ||||
|                     source: "goauthentik.io", | ||||
|                     context: "flow-executor", | ||||
|                                 token: token, | ||||
|                     token, | ||||
|                 }); | ||||
|             } | ||||
|                     </script> | ||||
|                 </body> | ||||
|             </html> | ||||
|         </head>`; | ||||
|         </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", | ||||
|             ); | ||||
|         this.captchaFrame.contentWindow?.document.close(); | ||||
|  | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         contentDocument.open(); | ||||
|  | ||||
|         contentDocument.write( | ||||
|             createIFrameHTMLWrapper( | ||||
|                 renderStaticHTMLUnsafe(iframeTemplate(captchaElement, this.challenge.jsUrl)), | ||||
|             ), | ||||
|         ); | ||||
|  | ||||
|         contentDocument.close(); | ||||
|     } | ||||
|  | ||||
|     renderBody() { | ||||
|  | ||||
		Reference in New Issue
	
	Block a user