Compare commits
	
		
			1 Commits
		
	
	
		
			dependabot
			...
			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",
 | 
					                "@formatjs/intl-listformat": "^7.5.7",
 | 
				
			||||||
                "@fortawesome/fontawesome-free": "^6.6.0",
 | 
					                "@fortawesome/fontawesome-free": "^6.6.0",
 | 
				
			||||||
                "@goauthentik/api": "^2025.2.4-1745325566",
 | 
					                "@goauthentik/api": "^2025.2.4-1745325566",
 | 
				
			||||||
                "@lit-labs/ssr": "^3.2.2",
 | 
					 | 
				
			||||||
                "@lit/context": "^1.1.2",
 | 
					                "@lit/context": "^1.1.2",
 | 
				
			||||||
                "@lit/localize": "^0.12.2",
 | 
					                "@lit/localize": "^0.12.2",
 | 
				
			||||||
                "@lit/reactive-element": "^2.0.4",
 | 
					                "@lit/reactive-element": "^2.0.4",
 | 
				
			||||||
@ -66,6 +65,7 @@
 | 
				
			|||||||
                "remark-gfm": "^4.0.1",
 | 
					                "remark-gfm": "^4.0.1",
 | 
				
			||||||
                "remark-mdx-frontmatter": "^5.0.0",
 | 
					                "remark-mdx-frontmatter": "^5.0.0",
 | 
				
			||||||
                "style-mod": "^4.1.2",
 | 
					                "style-mod": "^4.1.2",
 | 
				
			||||||
 | 
					                "trusted-types": "^2.0.0",
 | 
				
			||||||
                "ts-pattern": "^5.4.0",
 | 
					                "ts-pattern": "^5.4.0",
 | 
				
			||||||
                "unist-util-visit": "^5.0.0",
 | 
					                "unist-util-visit": "^5.0.0",
 | 
				
			||||||
                "webcomponent-qr-code": "^1.2.0",
 | 
					                "webcomponent-qr-code": "^1.2.0",
 | 
				
			||||||
@ -2281,47 +2281,11 @@
 | 
				
			|||||||
                "@lezer/lr": "^1.0.0"
 | 
					                "@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": {
 | 
					        "node_modules/@lit-labs/ssr-dom-shim": {
 | 
				
			||||||
            "version": "1.3.0",
 | 
					            "version": "1.3.0",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.3.0.tgz",
 | 
					            "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.3.0.tgz",
 | 
				
			||||||
            "integrity": "sha512-nQIWonJ6eFAvUUrSlwyHDm/aE8PBDu5kRpL0vHMg6K8fK3Diq1xdPjTnsJSwxABhaZ+5eBi1btQB5ShUTKo4nQ=="
 | 
					            "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": {
 | 
					        "node_modules/@lit/context": {
 | 
				
			||||||
            "version": "1.1.5",
 | 
					            "version": "1.1.5",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/@lit/context/-/context-1.1.5.tgz",
 | 
					            "resolved": "https://registry.npmjs.org/@lit/context/-/context-1.1.5.tgz",
 | 
				
			||||||
@ -3557,6 +3521,7 @@
 | 
				
			|||||||
            "version": "0.3.0",
 | 
					            "version": "0.3.0",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/@parse5/tools/-/tools-0.3.0.tgz",
 | 
					            "resolved": "https://registry.npmjs.org/@parse5/tools/-/tools-0.3.0.tgz",
 | 
				
			||||||
            "integrity": "sha512-zxRyTHkqb7WQMV8kTNBKWb1BeOFUKXBXTBWuxg9H9hfvQB3IwP6Iw2U75Ia5eyRxPNltmY7E8YAlz6zWwUnjKg==",
 | 
					            "integrity": "sha512-zxRyTHkqb7WQMV8kTNBKWb1BeOFUKXBXTBWuxg9H9hfvQB3IwP6Iw2U75Ia5eyRxPNltmY7E8YAlz6zWwUnjKg==",
 | 
				
			||||||
 | 
					            "dev": true,
 | 
				
			||||||
            "dependencies": {
 | 
					            "dependencies": {
 | 
				
			||||||
                "parse5": "^7.0.0"
 | 
					                "parse5": "^7.0.0"
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@ -10723,6 +10688,7 @@
 | 
				
			|||||||
            "version": "4.0.1",
 | 
					            "version": "4.0.1",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
 | 
					            "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
 | 
				
			||||||
            "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
 | 
					            "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
 | 
				
			||||||
 | 
					            "dev": true,
 | 
				
			||||||
            "engines": {
 | 
					            "engines": {
 | 
				
			||||||
                "node": ">= 12"
 | 
					                "node": ">= 12"
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@ -11343,6 +11309,7 @@
 | 
				
			|||||||
            "version": "5.18.1",
 | 
					            "version": "5.18.1",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
 | 
					            "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
 | 
				
			||||||
            "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==",
 | 
					            "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==",
 | 
				
			||||||
 | 
					            "dev": true,
 | 
				
			||||||
            "dependencies": {
 | 
					            "dependencies": {
 | 
				
			||||||
                "graceful-fs": "^4.2.4",
 | 
					                "graceful-fs": "^4.2.4",
 | 
				
			||||||
                "tapable": "^2.2.0"
 | 
					                "tapable": "^2.2.0"
 | 
				
			||||||
@ -13820,7 +13787,8 @@
 | 
				
			|||||||
        "node_modules/graceful-fs": {
 | 
					        "node_modules/graceful-fs": {
 | 
				
			||||||
            "version": "4.2.11",
 | 
					            "version": "4.2.11",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
 | 
					            "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": {
 | 
					        "node_modules/grapheme-splitter": {
 | 
				
			||||||
            "version": "1.0.4",
 | 
					            "version": "1.0.4",
 | 
				
			||||||
@ -18256,6 +18224,7 @@
 | 
				
			|||||||
            "version": "3.3.2",
 | 
					            "version": "3.3.2",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
 | 
					            "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
 | 
				
			||||||
            "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
 | 
					            "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
 | 
				
			||||||
 | 
					            "dev": true,
 | 
				
			||||||
            "dependencies": {
 | 
					            "dependencies": {
 | 
				
			||||||
                "data-uri-to-buffer": "^4.0.0",
 | 
					                "data-uri-to-buffer": "^4.0.0",
 | 
				
			||||||
                "fetch-blob": "^3.1.4",
 | 
					                "fetch-blob": "^3.1.4",
 | 
				
			||||||
@ -22373,6 +22342,7 @@
 | 
				
			|||||||
            "version": "2.2.1",
 | 
					            "version": "2.2.1",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
 | 
					            "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
 | 
				
			||||||
            "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
 | 
					            "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
 | 
				
			||||||
 | 
					            "dev": true,
 | 
				
			||||||
            "engines": {
 | 
					            "engines": {
 | 
				
			||||||
                "node": ">=6"
 | 
					                "node": ">=6"
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@ -22724,6 +22694,12 @@
 | 
				
			|||||||
                "url": "https://github.com/sponsors/wooorm"
 | 
					                "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": {
 | 
					        "node_modules/ts-api-utils": {
 | 
				
			||||||
            "version": "2.1.0",
 | 
					            "version": "2.1.0",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
 | 
					            "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",
 | 
					        "@formatjs/intl-listformat": "^7.5.7",
 | 
				
			||||||
        "@fortawesome/fontawesome-free": "^6.6.0",
 | 
					        "@fortawesome/fontawesome-free": "^6.6.0",
 | 
				
			||||||
        "@goauthentik/api": "^2025.2.4-1745325566",
 | 
					        "@goauthentik/api": "^2025.2.4-1745325566",
 | 
				
			||||||
        "@lit-labs/ssr": "^3.2.2",
 | 
					 | 
				
			||||||
        "@lit/context": "^1.1.2",
 | 
					        "@lit/context": "^1.1.2",
 | 
				
			||||||
        "@lit/localize": "^0.12.2",
 | 
					        "@lit/localize": "^0.12.2",
 | 
				
			||||||
        "@lit/reactive-element": "^2.0.4",
 | 
					        "@lit/reactive-element": "^2.0.4",
 | 
				
			||||||
@ -54,6 +53,7 @@
 | 
				
			|||||||
        "remark-gfm": "^4.0.1",
 | 
					        "remark-gfm": "^4.0.1",
 | 
				
			||||||
        "remark-mdx-frontmatter": "^5.0.0",
 | 
					        "remark-mdx-frontmatter": "^5.0.0",
 | 
				
			||||||
        "style-mod": "^4.1.2",
 | 
					        "style-mod": "^4.1.2",
 | 
				
			||||||
 | 
					        "trusted-types": "^2.0.0",
 | 
				
			||||||
        "ts-pattern": "^5.4.0",
 | 
					        "ts-pattern": "^5.4.0",
 | 
				
			||||||
        "unist-util-visit": "^5.0.0",
 | 
					        "unist-util-visit": "^5.0.0",
 | 
				
			||||||
        "webcomponent-qr-code": "^1.2.0",
 | 
					        "webcomponent-qr-code": "^1.2.0",
 | 
				
			||||||
 | 
				
			|||||||
@ -1,26 +1,110 @@
 | 
				
			|||||||
import type { Config as DOMPurifyConfig } from "dompurify";
 | 
					import type { Config as DOMPurifyConfig } from "dompurify";
 | 
				
			||||||
import DOMPurify from "dompurify";
 | 
					import DOMPurify from "dompurify";
 | 
				
			||||||
 | 
					import { trustedTypes } from "trusted-types";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { render } from "@lit-labs/ssr";
 | 
					import { render } from "lit";
 | 
				
			||||||
import { collectResult } from "@lit-labs/ssr/lib/render-result.js";
 | 
					 | 
				
			||||||
import { TemplateResult, html } from "lit";
 | 
					 | 
				
			||||||
import { unsafeHTML } from "lit/directives/unsafe-html.js";
 | 
					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 = {
 | 
					export const DOM_PURIFY_STRICT = {
 | 
				
			||||||
    ALLOWED_TAGS: ["#text"],
 | 
					    ALLOWED_TAGS: ["#text"],
 | 
				
			||||||
} as const satisfies DOMPurifyConfig;
 | 
					} 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 {
 | 
					    const result = container.innerHTML;
 | 
				
			||||||
    return html`${until(
 | 
					
 | 
				
			||||||
        (async () => {
 | 
					    return result;
 | 
				
			||||||
            const rendered = await renderStatic(input);
 | 
					 | 
				
			||||||
            const purified = DOMPurify.sanitize(rendered);
 | 
					 | 
				
			||||||
            return html`${unsafeHTML(purified)}`;
 | 
					 | 
				
			||||||
        })(),
 | 
					 | 
				
			||||||
    )}`;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										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 { AKElement } from "@goauthentik/elements/Base.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { msg } from "@lit/localize";
 | 
					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")
 | 
					@customElement("ak-brand-links")
 | 
				
			||||||
export class BrandLinks extends AKElement {
 | 
					export class BrandLinks extends AKElement {
 | 
				
			||||||
    static get styles() {
 | 
					    static get styles() {
 | 
				
			||||||
@ -33,13 +31,21 @@ export class BrandLinks extends AKElement {
 | 
				
			|||||||
    links: FooterLink[] = [];
 | 
					    links: FooterLink[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    render() {
 | 
					    render() {
 | 
				
			||||||
        const links = [...(this.links ?? []), poweredBy];
 | 
					        const links = [...(this.links ?? [])];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return html` <ul class="pf-c-list pf-m-inline">
 | 
					        return html` <ul class="pf-c-list pf-m-inline">
 | 
				
			||||||
            ${map(links, (link) =>
 | 
					            ${map(links, (link) => {
 | 
				
			||||||
                link.href
 | 
					                const children = sanitizeHTML(BrandedHTMLPolicy, link.name);
 | 
				
			||||||
                    ? purify(html`<li><a href="${link.href}">${link.name}</a></li>`)
 | 
					
 | 
				
			||||||
                    : html`<li><span>${link.name}</span></li>`,
 | 
					                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>`;
 | 
					        </ul>`;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,15 +1,16 @@
 | 
				
			|||||||
///<reference types="@hcaptcha/types"/>
 | 
					/// <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 "@goauthentik/elements/EmptyState";
 | 
				
			||||||
import { akEmptyState } from "@goauthentik/elements/EmptyState";
 | 
					import { akEmptyState } from "@goauthentik/elements/EmptyState";
 | 
				
			||||||
import { bound } from "@goauthentik/elements/decorators/bound";
 | 
					import { bound } from "@goauthentik/elements/decorators/bound";
 | 
				
			||||||
import "@goauthentik/elements/forms/FormElement";
 | 
					import "@goauthentik/elements/forms/FormElement";
 | 
				
			||||||
 | 
					import { createIFrameHTMLWrapper } from "@goauthentik/elements/utils/iframe";
 | 
				
			||||||
import { ListenerController } from "@goauthentik/elements/utils/listenerController.js";
 | 
					import { ListenerController } from "@goauthentik/elements/utils/listenerController.js";
 | 
				
			||||||
import { randomId } from "@goauthentik/elements/utils/randomId";
 | 
					import { randomId } from "@goauthentik/elements/utils/randomId";
 | 
				
			||||||
import "@goauthentik/flow/FormStatic";
 | 
					import "@goauthentik/flow/FormStatic";
 | 
				
			||||||
import { BaseStage } from "@goauthentik/flow/stages/base";
 | 
					import { BaseStage } from "@goauthentik/flow/stages/base";
 | 
				
			||||||
import { P, match } from "ts-pattern";
 | 
					import { P, match } from "ts-pattern";
 | 
				
			||||||
import type * as _ from "turnstile-types";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { msg } from "@lit/localize";
 | 
					import { msg } from "@lit/localize";
 | 
				
			||||||
import { CSSResult, PropertyValues, TemplateResult, css, html, nothing } from "lit";
 | 
					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
 | 
					// 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
 | 
					// margin, so adding 2rem of height to our container adds padding and prevents scroll bars or hidden
 | 
				
			||||||
// rendering.
 | 
					// 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) =>
 | 
					                window.parent.postMessage({
 | 
				
			||||||
    html`<!doctype html>
 | 
					                    message: "resize",
 | 
				
			||||||
        <head>
 | 
					                    source: "goauthentik.io",
 | 
				
			||||||
            <html>
 | 
					                    context: "flow-executor",
 | 
				
			||||||
                <body style="display:flex;flex-direction:row;justify-content:center;">
 | 
					                    size: { height },
 | 
				
			||||||
                    ${captchaElement}
 | 
					                });
 | 
				
			||||||
                    <script>
 | 
					            }).observe(document.querySelector(".ak-captcha-container"));
 | 
				
			||||||
                        new ResizeObserver((entries) => {
 | 
					        </script>
 | 
				
			||||||
                            const height =
 | 
					
 | 
				
			||||||
                                document.body.offsetHeight +
 | 
					        <script src=${challengeURL}></script>
 | 
				
			||||||
                                parseFloat(getComputedStyle(document.body).fontSize) * 2;
 | 
					
 | 
				
			||||||
                            window.parent.postMessage({
 | 
					        <script>
 | 
				
			||||||
                                message: "resize",
 | 
					            function callback(token) {
 | 
				
			||||||
                                source: "goauthentik.io",
 | 
					                window.parent.postMessage({
 | 
				
			||||||
                                context: "flow-executor",
 | 
					                    message: "captcha",
 | 
				
			||||||
                                size: { height },
 | 
					                    source: "goauthentik.io",
 | 
				
			||||||
                            });
 | 
					                    context: "flow-executor",
 | 
				
			||||||
                        }).observe(document.querySelector(".ak-captcha-container"));
 | 
					                    token,
 | 
				
			||||||
                    </script>
 | 
					                });
 | 
				
			||||||
                    <script src=${challengeUrl}></script>
 | 
					            }
 | 
				
			||||||
                    <script>
 | 
					        </script>`;
 | 
				
			||||||
                        function callback(token) {
 | 
					}
 | 
				
			||||||
                            window.parent.postMessage({
 | 
					 | 
				
			||||||
                                message: "captcha",
 | 
					 | 
				
			||||||
                                source: "goauthentik.io",
 | 
					 | 
				
			||||||
                                context: "flow-executor",
 | 
					 | 
				
			||||||
                                token: token,
 | 
					 | 
				
			||||||
                            });
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    </script>
 | 
					 | 
				
			||||||
                </body>
 | 
					 | 
				
			||||||
            </html>
 | 
					 | 
				
			||||||
        </head>`;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
@customElement("ak-stage-captcha")
 | 
					@customElement("ak-stage-captcha")
 | 
				
			||||||
export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeResponseRequest> {
 | 
					export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeResponseRequest> {
 | 
				
			||||||
@ -305,11 +302,25 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async renderFrame(captchaElement: TemplateResult) {
 | 
					    async renderFrame(captchaElement: TemplateResult) {
 | 
				
			||||||
        this.captchaFrame.contentWindow?.document.open();
 | 
					        const { contentDocument } = this.captchaFrame || {};
 | 
				
			||||||
        this.captchaFrame.contentWindow?.document.write(
 | 
					
 | 
				
			||||||
            await renderStatic(iframeTemplate(captchaElement, this.challenge.jsUrl)),
 | 
					        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() {
 | 
					    renderBody() {
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user