Compare commits
	
		
			2 Commits
		
	
	
		
			safari-cra
			...
			sfe-packag
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| a9373d60d0 | |||
| 82fadf587b | 
							
								
								
									
										5
									
								
								.github/workflows/api-ts-publish.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								.github/workflows/api-ts-publish.yml
									
									
									
									
										vendored
									
									
								
							@ -36,11 +36,6 @@ jobs:
 | 
				
			|||||||
        run: |
 | 
					        run: |
 | 
				
			||||||
          export VERSION=`node -e 'console.log(require("../gen-ts-api/package.json").version)'`
 | 
					          export VERSION=`node -e 'console.log(require("../gen-ts-api/package.json").version)'`
 | 
				
			||||||
          npm i @goauthentik/api@$VERSION
 | 
					          npm i @goauthentik/api@$VERSION
 | 
				
			||||||
      - name: Upgrade /web/packages/sfe
 | 
					 | 
				
			||||||
        working-directory: web/packages/sfe
 | 
					 | 
				
			||||||
        run: |
 | 
					 | 
				
			||||||
          export VERSION=`node -e 'console.log(require("../gen-ts-api/package.json").version)'`
 | 
					 | 
				
			||||||
          npm i @goauthentik/api@$VERSION
 | 
					 | 
				
			||||||
      - uses: peter-evans/create-pull-request@v7
 | 
					      - uses: peter-evans/create-pull-request@v7
 | 
				
			||||||
        id: cpr
 | 
					        id: cpr
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
 | 
				
			|||||||
@ -30,7 +30,6 @@ WORKDIR /work/web
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
RUN --mount=type=bind,target=/work/web/package.json,src=./web/package.json \
 | 
					RUN --mount=type=bind,target=/work/web/package.json,src=./web/package.json \
 | 
				
			||||||
    --mount=type=bind,target=/work/web/package-lock.json,src=./web/package-lock.json \
 | 
					    --mount=type=bind,target=/work/web/package-lock.json,src=./web/package-lock.json \
 | 
				
			||||||
    --mount=type=bind,target=/work/web/packages/sfe/package.json,src=./web/packages/sfe/package.json \
 | 
					 | 
				
			||||||
    --mount=type=bind,target=/work/web/scripts,src=./web/scripts \
 | 
					    --mount=type=bind,target=/work/web/scripts,src=./web/scripts \
 | 
				
			||||||
    --mount=type=cache,id=npm-web,sharing=shared,target=/root/.npm \
 | 
					    --mount=type=cache,id=npm-web,sharing=shared,target=/root/.npm \
 | 
				
			||||||
    npm ci --include=dev
 | 
					    npm ci --include=dev
 | 
				
			||||||
 | 
				
			|||||||
@ -49,6 +49,6 @@
 | 
				
			|||||||
        </main>
 | 
					        </main>
 | 
				
			||||||
        <span class="mt-3 mb-0 text-muted text-center">{% trans 'Powered by authentik' %}</span>
 | 
					        <span class="mt-3 mb-0 text-muted text-center">{% trans 'Powered by authentik' %}</span>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <script src="{% static 'dist/sfe/index.js' %}"></script>
 | 
					      <script src="{% static 'dist/sfe/main.js' %}"></script>
 | 
				
			||||||
    </body>
 | 
					    </body>
 | 
				
			||||||
</html>
 | 
					</html>
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										4
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										4
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@ -1,12 +1,12 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
    "name": "@goauthentik/authentik",
 | 
					    "name": "@goauthentik/authentik",
 | 
				
			||||||
    "version": "2025.2.1",
 | 
					    "version": "2025.2.3",
 | 
				
			||||||
    "lockfileVersion": 3,
 | 
					    "lockfileVersion": 3,
 | 
				
			||||||
    "requires": true,
 | 
					    "requires": true,
 | 
				
			||||||
    "packages": {
 | 
					    "packages": {
 | 
				
			||||||
        "": {
 | 
					        "": {
 | 
				
			||||||
            "name": "@goauthentik/authentik",
 | 
					            "name": "@goauthentik/authentik",
 | 
				
			||||||
            "version": "2025.2.1"
 | 
					            "version": "2025.2.3"
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										206
									
								
								web/authentication/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										206
									
								
								web/authentication/index.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,206 @@
 | 
				
			|||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @file WebAuthn utilities.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					import { fromByteArray } from "base64-js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//@ts-check
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//#region Type Definitions
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @typedef {object} Assertion
 | 
				
			||||||
 | 
					 * @property {string} id
 | 
				
			||||||
 | 
					 * @property {string} rawId
 | 
				
			||||||
 | 
					 * @property {string} type
 | 
				
			||||||
 | 
					 * @property {string} registrationClientExtensions
 | 
				
			||||||
 | 
					 * @property {object} response
 | 
				
			||||||
 | 
					 * @property {string} response.clientDataJSON
 | 
				
			||||||
 | 
					 * @property {string} response.attestationObject
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @typedef {object} AuthAssertion
 | 
				
			||||||
 | 
					 * @property {string} id
 | 
				
			||||||
 | 
					 * @property {string} rawId
 | 
				
			||||||
 | 
					 * @property {string} type
 | 
				
			||||||
 | 
					 * @property {string} assertionClientExtensions
 | 
				
			||||||
 | 
					 * @property {object} response
 | 
				
			||||||
 | 
					 * @property {string} response.clientDataJSON
 | 
				
			||||||
 | 
					 * @property {string} response.authenticatorData
 | 
				
			||||||
 | 
					 * @property {string} response.signature
 | 
				
			||||||
 | 
					 * @property {string | null} response.userHandle
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//#endregion
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//#region Encoding/Decoding
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Encodes a byte array into a URL-safe base64 string.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @param {Uint8Array} buffer
 | 
				
			||||||
 | 
					 * @returns {string}
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export function encodeBase64(buffer) {
 | 
				
			||||||
 | 
					    return fromByteArray(buffer).replace(/\+/g, "-").replace(/\//g, "_").replace(/[=]/g, "");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Encodes a byte array into a base64 string without URL-safe encoding, i.e., with padding.
 | 
				
			||||||
 | 
					 * @param {Uint8Array} buffer
 | 
				
			||||||
 | 
					 * @returns {string}
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export function encodeBase64Raw(buffer) {
 | 
				
			||||||
 | 
					    return fromByteArray(buffer).replace(/\+/g, "-").replace(/\//g, "_");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Decodes a base64 string into a byte array.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @param {string} input
 | 
				
			||||||
 | 
					 * @returns {Uint8Array}
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export function decodeBase64(input) {
 | 
				
			||||||
 | 
					    return Uint8Array.from(atob(input.replace(/_/g, "/").replace(/-/g, "+")), (c) =>
 | 
				
			||||||
 | 
					        c.charCodeAt(0),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//#endregion
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//#region Utility Functions
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Checks if the browser supports WebAuthn.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @returns {boolean}
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export function isWebAuthnSupported() {
 | 
				
			||||||
 | 
					    if ("credentials" in navigator) return true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (window.location.protocol === "http:" && window.location.hostname !== "localhost") {
 | 
				
			||||||
 | 
					        console.warn("WebAuthn requires this page to be accessed via HTTPS.");
 | 
				
			||||||
 | 
					        return false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.warn("WebAuthn not supported by browser.");
 | 
				
			||||||
 | 
					    return false;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Asserts that the browser supports WebAuthn and that we're in a secure context.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @throws {Error} If WebAuthn is not supported.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export function assertWebAuthnSupport() {
 | 
				
			||||||
 | 
					    // Is the navigator exposing the credentials API?
 | 
				
			||||||
 | 
					    if ("credentials" in navigator) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (window.location.protocol === "http:" && window.location.hostname !== "localhost") {
 | 
				
			||||||
 | 
					        throw new Error("WebAuthn requires this page to be accessed via HTTPS.");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    throw new Error("WebAuthn not supported by browser.");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Transforms items in the credentialCreateOptions generated on the server
 | 
				
			||||||
 | 
					 * into byte arrays expected by the navigator.credentials.create() call
 | 
				
			||||||
 | 
					 * @param {PublicKeyCredentialCreationOptions} credentialCreateOptions
 | 
				
			||||||
 | 
					 * @param {string} userID
 | 
				
			||||||
 | 
					 * @returns {PublicKeyCredentialCreationOptions}
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export function transformCredentialCreateOptions(credentialCreateOptions, userID) {
 | 
				
			||||||
 | 
					    const user = credentialCreateOptions.user;
 | 
				
			||||||
 | 
					    // Because json can't contain raw bytes, the server base64-encodes the User ID
 | 
				
			||||||
 | 
					    // So to get the base64 encoded byte array, we first need to convert it to a regular
 | 
				
			||||||
 | 
					    // string, then a byte array, re-encode it and wrap that in an array.
 | 
				
			||||||
 | 
					    const stringId = decodeURIComponent(window.atob(userID));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    user.id = decodeBase64(encodeBase64(decodeBase64(stringId)));
 | 
				
			||||||
 | 
					    const challenge = decodeBase64(credentialCreateOptions.challenge.toString());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					        ...credentialCreateOptions,
 | 
				
			||||||
 | 
					        challenge,
 | 
				
			||||||
 | 
					        user,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Transforms the binary data in the credential into base64 strings
 | 
				
			||||||
 | 
					 * for posting to the server.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @param {PublicKeyCredential} newAssertion
 | 
				
			||||||
 | 
					 * @returns {Assertion}
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export function transformNewAssertionForServer(newAssertion) {
 | 
				
			||||||
 | 
					    const response = /** @type {AuthenticatorAttestationResponse} */ (newAssertion.response);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const attObj = new Uint8Array(response.attestationObject);
 | 
				
			||||||
 | 
					    const clientDataJSON = new Uint8Array(newAssertion.response.clientDataJSON);
 | 
				
			||||||
 | 
					    const rawId = new Uint8Array(newAssertion.rawId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const registrationClientExtensions = newAssertion.getClientExtensionResults();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					        id: newAssertion.id,
 | 
				
			||||||
 | 
					        rawId: encodeBase64(rawId),
 | 
				
			||||||
 | 
					        type: newAssertion.type,
 | 
				
			||||||
 | 
					        registrationClientExtensions: JSON.stringify(registrationClientExtensions),
 | 
				
			||||||
 | 
					        response: {
 | 
				
			||||||
 | 
					            clientDataJSON: encodeBase64(clientDataJSON),
 | 
				
			||||||
 | 
					            attestationObject: encodeBase64(attObj),
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 *  Transforms  the items in the credentialRequestOptions generated on the server
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @param {PublicKeyCredentialRequestOptions} credentialRequestOptions
 | 
				
			||||||
 | 
					 * @returns {PublicKeyCredentialRequestOptions}
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export function transformCredentialRequestOptions(credentialRequestOptions) {
 | 
				
			||||||
 | 
					    const challenge = decodeBase64(credentialRequestOptions.challenge.toString());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const allowCredentials = (credentialRequestOptions.allowCredentials || []).map(
 | 
				
			||||||
 | 
					        (credentialDescriptor) => {
 | 
				
			||||||
 | 
					            const id = decodeBase64(credentialDescriptor.id.toString());
 | 
				
			||||||
 | 
					            return Object.assign({}, credentialDescriptor, { id });
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return Object.assign({}, credentialRequestOptions, {
 | 
				
			||||||
 | 
					        challenge,
 | 
				
			||||||
 | 
					        allowCredentials,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Encodes the binary data in the assertion into strings for posting to the server.
 | 
				
			||||||
 | 
					 * @param {PublicKeyCredential} newAssertion
 | 
				
			||||||
 | 
					 * @returns {AuthAssertion}
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export function transformAssertionForServer(newAssertion) {
 | 
				
			||||||
 | 
					    const response = /** @type {AuthenticatorAssertionResponse} */ (newAssertion.response);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const authData = new Uint8Array(response.authenticatorData);
 | 
				
			||||||
 | 
					    const clientDataJSON = new Uint8Array(response.clientDataJSON);
 | 
				
			||||||
 | 
					    const rawId = new Uint8Array(newAssertion.rawId);
 | 
				
			||||||
 | 
					    const sig = new Uint8Array(response.signature);
 | 
				
			||||||
 | 
					    const assertionClientExtensions = newAssertion.getClientExtensionResults();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					        id: newAssertion.id,
 | 
				
			||||||
 | 
					        rawId: encodeBase64(rawId),
 | 
				
			||||||
 | 
					        type: newAssertion.type,
 | 
				
			||||||
 | 
					        assertionClientExtensions: JSON.stringify(assertionClientExtensions),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        response: {
 | 
				
			||||||
 | 
					            clientDataJSON: encodeBase64Raw(clientDataJSON),
 | 
				
			||||||
 | 
					            signature: encodeBase64Raw(sig),
 | 
				
			||||||
 | 
					            authenticatorData: encodeBase64Raw(authData),
 | 
				
			||||||
 | 
					            userHandle: null,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -48,6 +48,9 @@ export default [
 | 
				
			|||||||
            "lit/no-template-bind": "error",
 | 
					            "lit/no-template-bind": "error",
 | 
				
			||||||
            "no-unused-vars": "off",
 | 
					            "no-unused-vars": "off",
 | 
				
			||||||
            "no-console": ["error", { allow: ["debug", "warn", "error"] }],
 | 
					            "no-console": ["error", { allow: ["debug", "warn", "error"] }],
 | 
				
			||||||
 | 
					            // TODO: TypeScript already handles this.
 | 
				
			||||||
 | 
					            // Remove after project-wide ESLint config is properly set up.
 | 
				
			||||||
 | 
					            "no-undef": "off",
 | 
				
			||||||
            "@typescript-eslint/ban-ts-comment": "off",
 | 
					            "@typescript-eslint/ban-ts-comment": "off",
 | 
				
			||||||
            "@typescript-eslint/no-unused-vars": [
 | 
					            "@typescript-eslint/no-unused-vars": [
 | 
				
			||||||
                "error",
 | 
					                "error",
 | 
				
			||||||
@ -71,8 +74,18 @@ export default [
 | 
				
			|||||||
                ...globals.node,
 | 
					                ...globals.node,
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        files: ["scripts/**/*.mjs", "*.ts", "*.mjs"],
 | 
					        files: [
 | 
				
			||||||
 | 
					            // TODO:Remove after project-wide ESLint config is properly set up.
 | 
				
			||||||
 | 
					            "scripts/**/*.mjs",
 | 
				
			||||||
 | 
					            "authentication/**/*.js",
 | 
				
			||||||
 | 
					            "sfe/**/*.js",
 | 
				
			||||||
 | 
					            "*.ts",
 | 
				
			||||||
 | 
					            "*.mjs",
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
        rules: {
 | 
					        rules: {
 | 
				
			||||||
 | 
					            "no-undef": "off",
 | 
				
			||||||
 | 
					            // TODO: TypeScript already handles this.
 | 
				
			||||||
 | 
					            // Remove after project-wide ESLint config is properly set up.
 | 
				
			||||||
            "no-unused-vars": "off",
 | 
					            "no-unused-vars": "off",
 | 
				
			||||||
            // We WANT our scripts to output to the console!
 | 
					            // We WANT our scripts to output to the console!
 | 
				
			||||||
            "no-console": "off",
 | 
					            "no-console": "off",
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										1914
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1914
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -57,9 +57,14 @@
 | 
				
			|||||||
        "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",
 | 
				
			||||||
        "yaml": "^2.5.1"
 | 
					        "yaml": "^2.5.1",
 | 
				
			||||||
 | 
					        "bootstrap": "^4.6.1",
 | 
				
			||||||
 | 
					        "formdata-polyfill": "^4.0.10",
 | 
				
			||||||
 | 
					        "jquery": "^3.7.1",
 | 
				
			||||||
 | 
					        "weakmap-polyfill": "^2.0.4"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "devDependencies": {
 | 
					    "devDependencies": {
 | 
				
			||||||
 | 
					        "@types/jquery": "^3.5.31",
 | 
				
			||||||
        "@eslint/js": "^9.11.1",
 | 
					        "@eslint/js": "^9.11.1",
 | 
				
			||||||
        "@hcaptcha/types": "^1.0.4",
 | 
					        "@hcaptcha/types": "^1.0.4",
 | 
				
			||||||
        "@lit/localize-tools": "^0.8.0",
 | 
					        "@lit/localize-tools": "^0.8.0",
 | 
				
			||||||
@ -90,6 +95,8 @@
 | 
				
			|||||||
        "@wdio/spec-reporter": "^9.1.2",
 | 
					        "@wdio/spec-reporter": "^9.1.2",
 | 
				
			||||||
        "chromedriver": "^131.0.1",
 | 
					        "chromedriver": "^131.0.1",
 | 
				
			||||||
        "esbuild": "^0.25.0",
 | 
					        "esbuild": "^0.25.0",
 | 
				
			||||||
 | 
					        "esbuild-plugin-copy": "^2.1.1",
 | 
				
			||||||
 | 
					        "esbuild-plugin-es5": "^2.1.1",
 | 
				
			||||||
        "esbuild-plugin-polyfill-node": "^0.3.0",
 | 
					        "esbuild-plugin-polyfill-node": "^0.3.0",
 | 
				
			||||||
        "esbuild-plugins-node-modules-polyfill": "^1.7.0",
 | 
					        "esbuild-plugins-node-modules-polyfill": "^1.7.0",
 | 
				
			||||||
        "eslint": "^9.11.1",
 | 
					        "eslint": "^9.11.1",
 | 
				
			||||||
@ -161,6 +168,12 @@
 | 
				
			|||||||
        "watch": "run-s build-locales esbuild:watch"
 | 
					        "watch": "run-s build-locales esbuild:watch"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "type": "module",
 | 
					    "type": "module",
 | 
				
			||||||
 | 
					    "exports": {
 | 
				
			||||||
 | 
					        "./package.json": "./package.json",
 | 
				
			||||||
 | 
					        "./paths": "./paths.js",
 | 
				
			||||||
 | 
					        "./authentication": "./authentication/index.js",
 | 
				
			||||||
 | 
					        "./scripts/*": "./scripts/*.mjs"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "wireit": {
 | 
					    "wireit": {
 | 
				
			||||||
        "build": {
 | 
					        "build": {
 | 
				
			||||||
            "#comment": [
 | 
					            "#comment": [
 | 
				
			||||||
@ -193,8 +206,7 @@
 | 
				
			|||||||
                "./dist/patternfly.min.css"
 | 
					                "./dist/patternfly.min.css"
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            "dependencies": [
 | 
					            "dependencies": [
 | 
				
			||||||
                "build-locales",
 | 
					                "build-locales"
 | 
				
			||||||
                "./packages/sfe:build"
 | 
					 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            "env": {
 | 
					            "env": {
 | 
				
			||||||
                "NODE_RUNNER": {
 | 
					                "NODE_RUNNER": {
 | 
				
			||||||
@ -204,12 +216,7 @@
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "build:sfe": {
 | 
					        "build:sfe": {
 | 
				
			||||||
            "dependencies": [
 | 
					            "command": "node scripts/build-sfe.mjs"
 | 
				
			||||||
                "./packages/sfe:build"
 | 
					 | 
				
			||||||
            ],
 | 
					 | 
				
			||||||
            "files": [
 | 
					 | 
				
			||||||
                "./packages/sfe/**/*.ts"
 | 
					 | 
				
			||||||
            ]
 | 
					 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "build-proxy": {
 | 
					        "build-proxy": {
 | 
				
			||||||
            "command": "node scripts/build-web.mjs --proxy",
 | 
					            "command": "node scripts/build-web.mjs --proxy",
 | 
				
			||||||
@ -242,11 +249,6 @@
 | 
				
			|||||||
                "lint:package"
 | 
					                "lint:package"
 | 
				
			||||||
            ]
 | 
					            ]
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "format:packages": {
 | 
					 | 
				
			||||||
            "dependencies": [
 | 
					 | 
				
			||||||
                "./packages/sfe:prettier"
 | 
					 | 
				
			||||||
            ]
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        "lint": {
 | 
					        "lint": {
 | 
				
			||||||
            "command": "eslint --max-warnings 0 --fix",
 | 
					            "command": "eslint --max-warnings 0 --fix",
 | 
				
			||||||
            "env": {
 | 
					            "env": {
 | 
				
			||||||
@ -274,11 +276,6 @@
 | 
				
			|||||||
            "shell": true,
 | 
					            "shell": true,
 | 
				
			||||||
            "command": "sh ./scripts/lint-lockfile.sh package-lock.json"
 | 
					            "command": "sh ./scripts/lint-lockfile.sh package-lock.json"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "lint:lockfiles": {
 | 
					 | 
				
			||||||
            "dependencies": [
 | 
					 | 
				
			||||||
                "./packages/sfe:lint:lockfile"
 | 
					 | 
				
			||||||
            ]
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        "lint:package": {
 | 
					        "lint:package": {
 | 
				
			||||||
            "command": "syncpack format -i '    '"
 | 
					            "command": "syncpack format -i '    '"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
@ -314,9 +311,7 @@
 | 
				
			|||||||
                "lint:spelling",
 | 
					                "lint:spelling",
 | 
				
			||||||
                "lint:package",
 | 
					                "lint:package",
 | 
				
			||||||
                "lint:lockfile",
 | 
					                "lint:lockfile",
 | 
				
			||||||
                "lint:lockfiles",
 | 
					                "lint:precommit"
 | 
				
			||||||
                "lint:precommit",
 | 
					 | 
				
			||||||
                "format:packages"
 | 
					 | 
				
			||||||
            ]
 | 
					            ]
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "prettier": {
 | 
					        "prettier": {
 | 
				
			||||||
 | 
				
			|||||||
@ -1,23 +0,0 @@
 | 
				
			|||||||
{
 | 
					 | 
				
			||||||
    "arrowParens": "always",
 | 
					 | 
				
			||||||
    "bracketSpacing": true,
 | 
					 | 
				
			||||||
    "embeddedLanguageFormatting": "auto",
 | 
					 | 
				
			||||||
    "htmlWhitespaceSensitivity": "css",
 | 
					 | 
				
			||||||
    "insertPragma": false,
 | 
					 | 
				
			||||||
    "jsxSingleQuote": false,
 | 
					 | 
				
			||||||
    "printWidth": 100,
 | 
					 | 
				
			||||||
    "proseWrap": "preserve",
 | 
					 | 
				
			||||||
    "quoteProps": "consistent",
 | 
					 | 
				
			||||||
    "requirePragma": false,
 | 
					 | 
				
			||||||
    "semi": true,
 | 
					 | 
				
			||||||
    "singleQuote": false,
 | 
					 | 
				
			||||||
    "tabWidth": 4,
 | 
					 | 
				
			||||||
    "trailingComma": "all",
 | 
					 | 
				
			||||||
    "useTabs": false,
 | 
					 | 
				
			||||||
    "vueIndentScriptAndStyle": false,
 | 
					 | 
				
			||||||
    "plugins": ["@trivago/prettier-plugin-sort-imports"],
 | 
					 | 
				
			||||||
    "importOrder": ["^(@?)lit(.*)$", "\\.css$", "^@goauthentik/api$", "^[./]"],
 | 
					 | 
				
			||||||
    "importOrderSeparation": true,
 | 
					 | 
				
			||||||
    "importOrderSortSpecifiers": true,
 | 
					 | 
				
			||||||
    "importOrderParserPlugins": ["typescript", "classProperties", "decorators-legacy"]
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,18 +0,0 @@
 | 
				
			|||||||
The MIT License (MIT)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Copyright (c) 2024 Authentik Security, Inc.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
 | 
					 | 
				
			||||||
associated documentation files (the "Software"), to deal in the Software without restriction,
 | 
					 | 
				
			||||||
including without limitation the rights to use, copy, modify, merge, publish, distribute,
 | 
					 | 
				
			||||||
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
 | 
					 | 
				
			||||||
furnished to do so, subject to the following conditions:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
The above copyright notice and this permission notice shall be included in all copies or substantial
 | 
					 | 
				
			||||||
portions of the Software.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
 | 
					 | 
				
			||||||
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 | 
					 | 
				
			||||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
 | 
					 | 
				
			||||||
OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 | 
					 | 
				
			||||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 | 
					 | 
				
			||||||
@ -1,68 +0,0 @@
 | 
				
			|||||||
{
 | 
					 | 
				
			||||||
    "name": "@goauthentik/web-sfe",
 | 
					 | 
				
			||||||
    "version": "0.0.0",
 | 
					 | 
				
			||||||
    "dependencies": {
 | 
					 | 
				
			||||||
        "@goauthentik/api": "^2024.6.0-1719577139",
 | 
					 | 
				
			||||||
        "base64-js": "^1.5.1",
 | 
					 | 
				
			||||||
        "bootstrap": "^4.6.1",
 | 
					 | 
				
			||||||
        "formdata-polyfill": "^4.0.10",
 | 
					 | 
				
			||||||
        "jquery": "^3.7.1",
 | 
					 | 
				
			||||||
        "weakmap-polyfill": "^2.0.4"
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "devDependencies": {
 | 
					 | 
				
			||||||
        "@rollup/plugin-commonjs": "^28.0.0",
 | 
					 | 
				
			||||||
        "@rollup/plugin-node-resolve": "^15.3.0",
 | 
					 | 
				
			||||||
        "@rollup/plugin-swc": "^0.4.0",
 | 
					 | 
				
			||||||
        "@swc/cli": "^0.4.0",
 | 
					 | 
				
			||||||
        "@swc/core": "^1.7.28",
 | 
					 | 
				
			||||||
        "@trivago/prettier-plugin-sort-imports": "^4.3.0",
 | 
					 | 
				
			||||||
        "@types/jquery": "^3.5.31",
 | 
					 | 
				
			||||||
        "lockfile-lint": "^4.14.0",
 | 
					 | 
				
			||||||
        "prettier": "^3.3.2",
 | 
					 | 
				
			||||||
        "rollup": "^4.23.0",
 | 
					 | 
				
			||||||
        "rollup-plugin-copy": "^3.5.0",
 | 
					 | 
				
			||||||
        "wireit": "^0.14.9"
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "license": "MIT",
 | 
					 | 
				
			||||||
    "optionalDependencies": {
 | 
					 | 
				
			||||||
        "@swc/core": "^1.7.28",
 | 
					 | 
				
			||||||
        "@swc/core-darwin-arm64": "^1.6.13",
 | 
					 | 
				
			||||||
        "@swc/core-darwin-x64": "^1.6.13",
 | 
					 | 
				
			||||||
        "@swc/core-linux-arm-gnueabihf": "^1.6.13",
 | 
					 | 
				
			||||||
        "@swc/core-linux-arm64-gnu": "^1.6.13",
 | 
					 | 
				
			||||||
        "@swc/core-linux-arm64-musl": "^1.6.13",
 | 
					 | 
				
			||||||
        "@swc/core-linux-x64-gnu": "^1.6.13",
 | 
					 | 
				
			||||||
        "@swc/core-linux-x64-musl": "^1.6.13",
 | 
					 | 
				
			||||||
        "@swc/core-win32-arm64-msvc": "^1.6.13",
 | 
					 | 
				
			||||||
        "@swc/core-win32-ia32-msvc": "^1.6.13",
 | 
					 | 
				
			||||||
        "@swc/core-win32-x64-msvc": "^1.6.13"
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "private": true,
 | 
					 | 
				
			||||||
    "scripts": {
 | 
					 | 
				
			||||||
        "build": "wireit",
 | 
					 | 
				
			||||||
        "lint:lockfile": "wireit",
 | 
					 | 
				
			||||||
        "prettier": "prettier --write ./src ./tsconfig.json ./rollup.config.js ./package.json",
 | 
					 | 
				
			||||||
        "watch": "rollup -w -c rollup.config.js --bundleConfigAsCjs"
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "wireit": {
 | 
					 | 
				
			||||||
        "build:sfe": {
 | 
					 | 
				
			||||||
            "command": "rollup -c rollup.config.js --bundleConfigAsCjs",
 | 
					 | 
				
			||||||
            "files": [
 | 
					 | 
				
			||||||
                "../../node_modules/bootstrap/dist/css/bootstrap.min.css",
 | 
					 | 
				
			||||||
                "src/index.ts"
 | 
					 | 
				
			||||||
            ],
 | 
					 | 
				
			||||||
            "output": [
 | 
					 | 
				
			||||||
                "./dist/sfe/*"
 | 
					 | 
				
			||||||
            ]
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        "build": {
 | 
					 | 
				
			||||||
            "command": "mkdir -p ../../dist/sfe && cp -r dist/sfe/* ../../dist/sfe",
 | 
					 | 
				
			||||||
            "dependencies": [
 | 
					 | 
				
			||||||
                "build:sfe"
 | 
					 | 
				
			||||||
            ]
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        "lint:lockfile": {
 | 
					 | 
				
			||||||
            "command": "lockfile-lint --path package.json --type npm --allowed-hosts npm --validate-https"
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,43 +0,0 @@
 | 
				
			|||||||
import commonjs from "@rollup/plugin-commonjs";
 | 
					 | 
				
			||||||
import resolve from "@rollup/plugin-node-resolve";
 | 
					 | 
				
			||||||
import swc from "@rollup/plugin-swc";
 | 
					 | 
				
			||||||
import copy from "rollup-plugin-copy";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default {
 | 
					 | 
				
			||||||
    input: "src/index.ts",
 | 
					 | 
				
			||||||
    output: {
 | 
					 | 
				
			||||||
        dir: "./dist/sfe",
 | 
					 | 
				
			||||||
        format: "cjs",
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    context: "window",
 | 
					 | 
				
			||||||
    plugins: [
 | 
					 | 
				
			||||||
        copy({
 | 
					 | 
				
			||||||
            targets: [
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                    src: "../../node_modules/bootstrap/dist/css/bootstrap.min.css",
 | 
					 | 
				
			||||||
                    dest: "./dist/sfe",
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
            ],
 | 
					 | 
				
			||||||
        }),
 | 
					 | 
				
			||||||
        resolve({ browser: true }),
 | 
					 | 
				
			||||||
        commonjs(),
 | 
					 | 
				
			||||||
        swc({
 | 
					 | 
				
			||||||
            swc: {
 | 
					 | 
				
			||||||
                jsc: {
 | 
					 | 
				
			||||||
                    loose: false,
 | 
					 | 
				
			||||||
                    externalHelpers: false,
 | 
					 | 
				
			||||||
                    // Requires v1.2.50 or upper and requires target to be es2016 or upper.
 | 
					 | 
				
			||||||
                    keepClassNames: false,
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
                minify: false,
 | 
					 | 
				
			||||||
                env: {
 | 
					 | 
				
			||||||
                    targets: {
 | 
					 | 
				
			||||||
                        edge: "17",
 | 
					 | 
				
			||||||
                        ie: "11",
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
                    mode: "entry",
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
        }),
 | 
					 | 
				
			||||||
    ],
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
@ -1,527 +0,0 @@
 | 
				
			|||||||
import { fromByteArray } from "base64-js";
 | 
					 | 
				
			||||||
import "formdata-polyfill";
 | 
					 | 
				
			||||||
import $ from "jquery";
 | 
					 | 
				
			||||||
import "weakmap-polyfill";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
    type AuthenticatorValidationChallenge,
 | 
					 | 
				
			||||||
    type AutosubmitChallenge,
 | 
					 | 
				
			||||||
    type ChallengeTypes,
 | 
					 | 
				
			||||||
    ChallengeTypesFromJSON,
 | 
					 | 
				
			||||||
    type ContextualFlowInfo,
 | 
					 | 
				
			||||||
    type DeviceChallenge,
 | 
					 | 
				
			||||||
    type ErrorDetail,
 | 
					 | 
				
			||||||
    type IdentificationChallenge,
 | 
					 | 
				
			||||||
    type PasswordChallenge,
 | 
					 | 
				
			||||||
    type RedirectChallenge,
 | 
					 | 
				
			||||||
} from "@goauthentik/api";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
interface GlobalAuthentik {
 | 
					 | 
				
			||||||
    brand: {
 | 
					 | 
				
			||||||
        branding_logo: string;
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
    api: {
 | 
					 | 
				
			||||||
        base: string;
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function ak(): GlobalAuthentik {
 | 
					 | 
				
			||||||
    return (
 | 
					 | 
				
			||||||
        window as unknown as {
 | 
					 | 
				
			||||||
            authentik: GlobalAuthentik;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    ).authentik;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class SimpleFlowExecutor {
 | 
					 | 
				
			||||||
    challenge?: ChallengeTypes;
 | 
					 | 
				
			||||||
    flowSlug: string;
 | 
					 | 
				
			||||||
    container: HTMLDivElement;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    constructor(container: HTMLDivElement) {
 | 
					 | 
				
			||||||
        this.flowSlug = window.location.pathname.split("/")[3];
 | 
					 | 
				
			||||||
        this.container = container;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    get apiURL() {
 | 
					 | 
				
			||||||
        return `${ak().api.base}api/v3/flows/executor/${this.flowSlug}/?query=${encodeURIComponent(window.location.search.substring(1))}`;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    start() {
 | 
					 | 
				
			||||||
        $.ajax({
 | 
					 | 
				
			||||||
            type: "GET",
 | 
					 | 
				
			||||||
            url: this.apiURL,
 | 
					 | 
				
			||||||
            success: (data) => {
 | 
					 | 
				
			||||||
                this.challenge = ChallengeTypesFromJSON(data);
 | 
					 | 
				
			||||||
                this.renderChallenge();
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    submit(data: { [key: string]: unknown } | FormData) {
 | 
					 | 
				
			||||||
        $("button[type=submit]").addClass("disabled")
 | 
					 | 
				
			||||||
            .html(`<span class="spinner-border spinner-border-sm" aria-hidden="true"></span>
 | 
					 | 
				
			||||||
                <span role="status">Loading...</span>`);
 | 
					 | 
				
			||||||
        let finalData: { [key: string]: unknown } = {};
 | 
					 | 
				
			||||||
        if (data instanceof FormData) {
 | 
					 | 
				
			||||||
            finalData = {};
 | 
					 | 
				
			||||||
            data.forEach((value, key) => {
 | 
					 | 
				
			||||||
                finalData[key] = value;
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            finalData = data;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $.ajax({
 | 
					 | 
				
			||||||
            type: "POST",
 | 
					 | 
				
			||||||
            url: this.apiURL,
 | 
					 | 
				
			||||||
            data: JSON.stringify(finalData),
 | 
					 | 
				
			||||||
            success: (data) => {
 | 
					 | 
				
			||||||
                this.challenge = ChallengeTypesFromJSON(data);
 | 
					 | 
				
			||||||
                this.renderChallenge();
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            contentType: "application/json",
 | 
					 | 
				
			||||||
            dataType: "json",
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    renderChallenge() {
 | 
					 | 
				
			||||||
        switch (this.challenge?.component) {
 | 
					 | 
				
			||||||
            case "ak-stage-identification":
 | 
					 | 
				
			||||||
                new IdentificationStage(this, this.challenge).render();
 | 
					 | 
				
			||||||
                return;
 | 
					 | 
				
			||||||
            case "ak-stage-password":
 | 
					 | 
				
			||||||
                new PasswordStage(this, this.challenge).render();
 | 
					 | 
				
			||||||
                return;
 | 
					 | 
				
			||||||
            case "xak-flow-redirect":
 | 
					 | 
				
			||||||
                new RedirectStage(this, this.challenge).render();
 | 
					 | 
				
			||||||
                return;
 | 
					 | 
				
			||||||
            case "ak-stage-autosubmit":
 | 
					 | 
				
			||||||
                new AutosubmitStage(this, this.challenge).render();
 | 
					 | 
				
			||||||
                return;
 | 
					 | 
				
			||||||
            case "ak-stage-authenticator-validate":
 | 
					 | 
				
			||||||
                new AuthenticatorValidateStage(this, this.challenge).render();
 | 
					 | 
				
			||||||
                return;
 | 
					 | 
				
			||||||
            default:
 | 
					 | 
				
			||||||
                this.container.innerText = "Unsupported stage: " + this.challenge?.component;
 | 
					 | 
				
			||||||
                return;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface FlowInfoChallenge {
 | 
					 | 
				
			||||||
    flowInfo?: ContextualFlowInfo;
 | 
					 | 
				
			||||||
    responseErrors?: {
 | 
					 | 
				
			||||||
        [key: string]: Array<ErrorDetail>;
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class Stage<T extends FlowInfoChallenge> {
 | 
					 | 
				
			||||||
    constructor(
 | 
					 | 
				
			||||||
        public executor: SimpleFlowExecutor,
 | 
					 | 
				
			||||||
        public challenge: T,
 | 
					 | 
				
			||||||
    ) {}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    error(fieldName: string) {
 | 
					 | 
				
			||||||
        if (!this.challenge.responseErrors) {
 | 
					 | 
				
			||||||
            return [];
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return this.challenge.responseErrors[fieldName] || [];
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    renderInputError(fieldName: string) {
 | 
					 | 
				
			||||||
        return `${this.error(fieldName)
 | 
					 | 
				
			||||||
            .map((error) => {
 | 
					 | 
				
			||||||
                return `<div class="invalid-feedback">
 | 
					 | 
				
			||||||
                    ${error.string}
 | 
					 | 
				
			||||||
                </div>`;
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
            .join("")}`;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    renderNonFieldErrors() {
 | 
					 | 
				
			||||||
        return `${this.error("non_field_errors")
 | 
					 | 
				
			||||||
            .map((error) => {
 | 
					 | 
				
			||||||
                return `<div class="alert alert-danger" role="alert">
 | 
					 | 
				
			||||||
                    ${error.string}
 | 
					 | 
				
			||||||
                </div>`;
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
            .join("")}`;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    html(html: string) {
 | 
					 | 
				
			||||||
        this.executor.container.innerHTML = html;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    render() {
 | 
					 | 
				
			||||||
        throw new Error("Abstract method");
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const IS_INVALID = "is-invalid";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class IdentificationStage extends Stage<IdentificationChallenge> {
 | 
					 | 
				
			||||||
    render() {
 | 
					 | 
				
			||||||
        this.html(`
 | 
					 | 
				
			||||||
            <form id="ident-form">
 | 
					 | 
				
			||||||
                <img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
 | 
					 | 
				
			||||||
                <h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
 | 
					 | 
				
			||||||
                ${
 | 
					 | 
				
			||||||
                    this.challenge.applicationPre
 | 
					 | 
				
			||||||
                        ? `<p>
 | 
					 | 
				
			||||||
                              Log in to continue to ${this.challenge.applicationPre}.
 | 
					 | 
				
			||||||
                          </p>`
 | 
					 | 
				
			||||||
                        : ""
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                <div class="form-label-group my-3 has-validation">
 | 
					 | 
				
			||||||
                    <input type="text" autofocus class="form-control" name="uid_field" placeholder="Email / Username">
 | 
					 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
                ${
 | 
					 | 
				
			||||||
                    this.challenge.passwordFields
 | 
					 | 
				
			||||||
                        ? `<div class="form-label-group my-3 has-validation">
 | 
					 | 
				
			||||||
                                <input type="password" class="form-control ${this.error("password").length > 0 ? IS_INVALID : ""}" name="password" placeholder="Password">
 | 
					 | 
				
			||||||
                                ${this.renderInputError("password")}
 | 
					 | 
				
			||||||
                        </div>`
 | 
					 | 
				
			||||||
                        : ""
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                ${this.renderNonFieldErrors()}
 | 
					 | 
				
			||||||
                <button class="btn btn-primary w-100 py-2" type="submit">${this.challenge.primaryAction}</button>
 | 
					 | 
				
			||||||
            </form>`);
 | 
					 | 
				
			||||||
        $("#ident-form input[name=uid_field]").trigger("focus");
 | 
					 | 
				
			||||||
        $("#ident-form").on("submit", (ev) => {
 | 
					 | 
				
			||||||
            ev.preventDefault();
 | 
					 | 
				
			||||||
            const data = new FormData(ev.target as HTMLFormElement);
 | 
					 | 
				
			||||||
            this.executor.submit(data);
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class PasswordStage extends Stage<PasswordChallenge> {
 | 
					 | 
				
			||||||
    render() {
 | 
					 | 
				
			||||||
        this.html(`
 | 
					 | 
				
			||||||
            <form id="password-form">
 | 
					 | 
				
			||||||
                <img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
 | 
					 | 
				
			||||||
                <h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
 | 
					 | 
				
			||||||
                <div class="form-label-group my-3 has-validation">
 | 
					 | 
				
			||||||
                    <input type="password" autofocus class="form-control ${this.error("password").length > 0 ? IS_INVALID : ""}" name="password" placeholder="Password">
 | 
					 | 
				
			||||||
                    ${this.renderInputError("password")}
 | 
					 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
                <button class="btn btn-primary w-100 py-2" type="submit">Continue</button>
 | 
					 | 
				
			||||||
            </form>`);
 | 
					 | 
				
			||||||
        $("#password-form input").trigger("focus");
 | 
					 | 
				
			||||||
        $("#password-form").on("submit", (ev) => {
 | 
					 | 
				
			||||||
            ev.preventDefault();
 | 
					 | 
				
			||||||
            const data = new FormData(ev.target as HTMLFormElement);
 | 
					 | 
				
			||||||
            this.executor.submit(data);
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class RedirectStage extends Stage<RedirectChallenge> {
 | 
					 | 
				
			||||||
    render() {
 | 
					 | 
				
			||||||
        window.location.assign(this.challenge.to);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class AutosubmitStage extends Stage<AutosubmitChallenge> {
 | 
					 | 
				
			||||||
    render() {
 | 
					 | 
				
			||||||
        this.html(`
 | 
					 | 
				
			||||||
            <form id="autosubmit-form" action="${this.challenge.url}" method="POST">
 | 
					 | 
				
			||||||
                <img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
 | 
					 | 
				
			||||||
                <h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
 | 
					 | 
				
			||||||
                ${Object.entries(this.challenge.attrs).map(([key, value]) => {
 | 
					 | 
				
			||||||
                    return `<input
 | 
					 | 
				
			||||||
                            type="hidden"
 | 
					 | 
				
			||||||
                            name="${key}"
 | 
					 | 
				
			||||||
                            value="${value}"
 | 
					 | 
				
			||||||
                        />`;
 | 
					 | 
				
			||||||
                })}
 | 
					 | 
				
			||||||
                <div class="d-flex justify-content-center">
 | 
					 | 
				
			||||||
                    <div class="spinner-border" role="status">
 | 
					 | 
				
			||||||
                        <span class="sr-only">Loading...</span>
 | 
					 | 
				
			||||||
                    </div>
 | 
					 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
            </form>`);
 | 
					 | 
				
			||||||
        $("#autosubmit-form").submit();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface Assertion {
 | 
					 | 
				
			||||||
    id: string;
 | 
					 | 
				
			||||||
    rawId: string;
 | 
					 | 
				
			||||||
    type: string;
 | 
					 | 
				
			||||||
    registrationClientExtensions: string;
 | 
					 | 
				
			||||||
    response: {
 | 
					 | 
				
			||||||
        clientDataJSON: string;
 | 
					 | 
				
			||||||
        attestationObject: string;
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface AuthAssertion {
 | 
					 | 
				
			||||||
    id: string;
 | 
					 | 
				
			||||||
    rawId: string;
 | 
					 | 
				
			||||||
    type: string;
 | 
					 | 
				
			||||||
    assertionClientExtensions: string;
 | 
					 | 
				
			||||||
    response: {
 | 
					 | 
				
			||||||
        clientDataJSON: string;
 | 
					 | 
				
			||||||
        authenticatorData: string;
 | 
					 | 
				
			||||||
        signature: string;
 | 
					 | 
				
			||||||
        userHandle: string | null;
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge> {
 | 
					 | 
				
			||||||
    deviceChallenge?: DeviceChallenge;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    b64enc(buf: Uint8Array): string {
 | 
					 | 
				
			||||||
        return fromByteArray(buf).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    b64RawEnc(buf: Uint8Array): string {
 | 
					 | 
				
			||||||
        return fromByteArray(buf).replace(/\+/g, "-").replace(/\//g, "_");
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    u8arr(input: string): Uint8Array {
 | 
					 | 
				
			||||||
        return Uint8Array.from(atob(input.replace(/_/g, "/").replace(/-/g, "+")), (c) =>
 | 
					 | 
				
			||||||
            c.charCodeAt(0),
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    checkWebAuthnSupport(): boolean {
 | 
					 | 
				
			||||||
        if ("credentials" in navigator) {
 | 
					 | 
				
			||||||
            return true;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if (window.location.protocol === "http:" && window.location.hostname !== "localhost") {
 | 
					 | 
				
			||||||
            console.warn("WebAuthn requires this page to be accessed via HTTPS.");
 | 
					 | 
				
			||||||
            return false;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        console.warn("WebAuthn not supported by browser.");
 | 
					 | 
				
			||||||
        return false;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Transforms items in the credentialCreateOptions generated on the server
 | 
					 | 
				
			||||||
     * into byte arrays expected by the navigator.credentials.create() call
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    transformCredentialCreateOptions(
 | 
					 | 
				
			||||||
        credentialCreateOptions: PublicKeyCredentialCreationOptions,
 | 
					 | 
				
			||||||
        userId: string,
 | 
					 | 
				
			||||||
    ): PublicKeyCredentialCreationOptions {
 | 
					 | 
				
			||||||
        const user = credentialCreateOptions.user;
 | 
					 | 
				
			||||||
        // Because json can't contain raw bytes, the server base64-encodes the User ID
 | 
					 | 
				
			||||||
        // So to get the base64 encoded byte array, we first need to convert it to a regular
 | 
					 | 
				
			||||||
        // string, then a byte array, re-encode it and wrap that in an array.
 | 
					 | 
				
			||||||
        const stringId = decodeURIComponent(window.atob(userId));
 | 
					 | 
				
			||||||
        user.id = this.u8arr(this.b64enc(this.u8arr(stringId)));
 | 
					 | 
				
			||||||
        const challenge = this.u8arr(credentialCreateOptions.challenge.toString());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return Object.assign({}, credentialCreateOptions, {
 | 
					 | 
				
			||||||
            challenge,
 | 
					 | 
				
			||||||
            user,
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Transforms the binary data in the credential into base64 strings
 | 
					 | 
				
			||||||
     * for posting to the server.
 | 
					 | 
				
			||||||
     * @param {PublicKeyCredential} newAssertion
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    transformNewAssertionForServer(newAssertion: PublicKeyCredential): Assertion {
 | 
					 | 
				
			||||||
        const attObj = new Uint8Array(
 | 
					 | 
				
			||||||
            (newAssertion.response as AuthenticatorAttestationResponse).attestationObject,
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        const clientDataJSON = new Uint8Array(newAssertion.response.clientDataJSON);
 | 
					 | 
				
			||||||
        const rawId = new Uint8Array(newAssertion.rawId);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const registrationClientExtensions = newAssertion.getClientExtensionResults();
 | 
					 | 
				
			||||||
        return {
 | 
					 | 
				
			||||||
            id: newAssertion.id,
 | 
					 | 
				
			||||||
            rawId: this.b64enc(rawId),
 | 
					 | 
				
			||||||
            type: newAssertion.type,
 | 
					 | 
				
			||||||
            registrationClientExtensions: JSON.stringify(registrationClientExtensions),
 | 
					 | 
				
			||||||
            response: {
 | 
					 | 
				
			||||||
                clientDataJSON: this.b64enc(clientDataJSON),
 | 
					 | 
				
			||||||
                attestationObject: this.b64enc(attObj),
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    transformCredentialRequestOptions(
 | 
					 | 
				
			||||||
        credentialRequestOptions: PublicKeyCredentialRequestOptions,
 | 
					 | 
				
			||||||
    ): PublicKeyCredentialRequestOptions {
 | 
					 | 
				
			||||||
        const challenge = this.u8arr(credentialRequestOptions.challenge.toString());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const allowCredentials = (credentialRequestOptions.allowCredentials || []).map(
 | 
					 | 
				
			||||||
            (credentialDescriptor) => {
 | 
					 | 
				
			||||||
                const id = this.u8arr(credentialDescriptor.id.toString());
 | 
					 | 
				
			||||||
                return Object.assign({}, credentialDescriptor, { id });
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return Object.assign({}, credentialRequestOptions, {
 | 
					 | 
				
			||||||
            challenge,
 | 
					 | 
				
			||||||
            allowCredentials,
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Encodes the binary data in the assertion into strings for posting to the server.
 | 
					 | 
				
			||||||
     * @param {PublicKeyCredential} newAssertion
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    transformAssertionForServer(newAssertion: PublicKeyCredential): AuthAssertion {
 | 
					 | 
				
			||||||
        const response = newAssertion.response as AuthenticatorAssertionResponse;
 | 
					 | 
				
			||||||
        const authData = new Uint8Array(response.authenticatorData);
 | 
					 | 
				
			||||||
        const clientDataJSON = new Uint8Array(response.clientDataJSON);
 | 
					 | 
				
			||||||
        const rawId = new Uint8Array(newAssertion.rawId);
 | 
					 | 
				
			||||||
        const sig = new Uint8Array(response.signature);
 | 
					 | 
				
			||||||
        const assertionClientExtensions = newAssertion.getClientExtensionResults();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return {
 | 
					 | 
				
			||||||
            id: newAssertion.id,
 | 
					 | 
				
			||||||
            rawId: this.b64enc(rawId),
 | 
					 | 
				
			||||||
            type: newAssertion.type,
 | 
					 | 
				
			||||||
            assertionClientExtensions: JSON.stringify(assertionClientExtensions),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            response: {
 | 
					 | 
				
			||||||
                clientDataJSON: this.b64RawEnc(clientDataJSON),
 | 
					 | 
				
			||||||
                signature: this.b64RawEnc(sig),
 | 
					 | 
				
			||||||
                authenticatorData: this.b64RawEnc(authData),
 | 
					 | 
				
			||||||
                userHandle: null,
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    render() {
 | 
					 | 
				
			||||||
        if (!this.deviceChallenge) {
 | 
					 | 
				
			||||||
            return this.renderChallengePicker();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        switch (this.deviceChallenge.deviceClass) {
 | 
					 | 
				
			||||||
            case "static":
 | 
					 | 
				
			||||||
            case "totp":
 | 
					 | 
				
			||||||
                this.renderCodeInput();
 | 
					 | 
				
			||||||
                break;
 | 
					 | 
				
			||||||
            case "webauthn":
 | 
					 | 
				
			||||||
                this.renderWebauthn();
 | 
					 | 
				
			||||||
                break;
 | 
					 | 
				
			||||||
            default:
 | 
					 | 
				
			||||||
                break;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    renderChallengePicker() {
 | 
					 | 
				
			||||||
        const challenges = this.challenge.deviceChallenges.filter((challenge) =>
 | 
					 | 
				
			||||||
            challenge.deviceClass === "webauthn" && !this.checkWebAuthnSupport()
 | 
					 | 
				
			||||||
                ? undefined
 | 
					 | 
				
			||||||
                : challenge,
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        this.html(`<form id="picker-form">
 | 
					 | 
				
			||||||
                <img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
 | 
					 | 
				
			||||||
                <h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
 | 
					 | 
				
			||||||
                ${
 | 
					 | 
				
			||||||
                    challenges.length > 0
 | 
					 | 
				
			||||||
                        ? "<p>Select an authentication method.</p>"
 | 
					 | 
				
			||||||
                        : `
 | 
					 | 
				
			||||||
                    <p>No compatible authentication method available</p>
 | 
					 | 
				
			||||||
                    `
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                ${challenges
 | 
					 | 
				
			||||||
                    .map((challenge) => {
 | 
					 | 
				
			||||||
                        let label = undefined;
 | 
					 | 
				
			||||||
                        switch (challenge.deviceClass) {
 | 
					 | 
				
			||||||
                            case "static":
 | 
					 | 
				
			||||||
                                label = "Recovery keys";
 | 
					 | 
				
			||||||
                                break;
 | 
					 | 
				
			||||||
                            case "totp":
 | 
					 | 
				
			||||||
                                label = "Traditional authenticator";
 | 
					 | 
				
			||||||
                                break;
 | 
					 | 
				
			||||||
                            case "webauthn":
 | 
					 | 
				
			||||||
                                label = "Security key";
 | 
					 | 
				
			||||||
                                break;
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                        if (!label) {
 | 
					 | 
				
			||||||
                            return "";
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                        return `<div class="form-label-group my-3 has-validation">
 | 
					 | 
				
			||||||
                            <button id="${challenge.deviceClass}-${challenge.deviceUid}" class="btn btn-secondary w-100 py-2" type="button">
 | 
					 | 
				
			||||||
                                ${label}
 | 
					 | 
				
			||||||
                            </button>
 | 
					 | 
				
			||||||
                        </div>`;
 | 
					 | 
				
			||||||
                    })
 | 
					 | 
				
			||||||
                    .join("")}
 | 
					 | 
				
			||||||
            </form>`);
 | 
					 | 
				
			||||||
        this.challenge.deviceChallenges.forEach((challenge) => {
 | 
					 | 
				
			||||||
            $(`#picker-form button#${challenge.deviceClass}-${challenge.deviceUid}`).on(
 | 
					 | 
				
			||||||
                "click",
 | 
					 | 
				
			||||||
                () => {
 | 
					 | 
				
			||||||
                    this.deviceChallenge = challenge;
 | 
					 | 
				
			||||||
                    this.render();
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    renderCodeInput() {
 | 
					 | 
				
			||||||
        this.html(`
 | 
					 | 
				
			||||||
            <form id="totp-form">
 | 
					 | 
				
			||||||
                <img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
 | 
					 | 
				
			||||||
                <h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
 | 
					 | 
				
			||||||
                <div class="form-label-group my-3 has-validation">
 | 
					 | 
				
			||||||
                    <input type="text" autofocus class="form-control ${this.error("code").length > 0 ? IS_INVALID : ""}" name="code" placeholder="Please enter your code" autocomplete="one-time-code">
 | 
					 | 
				
			||||||
                    ${this.renderInputError("code")}
 | 
					 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
                <button class="btn btn-primary w-100 py-2" type="submit">Continue</button>
 | 
					 | 
				
			||||||
            </form>`);
 | 
					 | 
				
			||||||
        $("#totp-form input").trigger("focus");
 | 
					 | 
				
			||||||
        $("#totp-form").on("submit", (ev) => {
 | 
					 | 
				
			||||||
            ev.preventDefault();
 | 
					 | 
				
			||||||
            const data = new FormData(ev.target as HTMLFormElement);
 | 
					 | 
				
			||||||
            this.executor.submit(data);
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    renderWebauthn() {
 | 
					 | 
				
			||||||
        this.html(`
 | 
					 | 
				
			||||||
            <form id="totp-form">
 | 
					 | 
				
			||||||
                <img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
 | 
					 | 
				
			||||||
                <h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
 | 
					 | 
				
			||||||
                <div class="d-flex justify-content-center">
 | 
					 | 
				
			||||||
                    <div class="spinner-border" role="status">
 | 
					 | 
				
			||||||
                        <span class="sr-only">Loading...</span>
 | 
					 | 
				
			||||||
                    </div>
 | 
					 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
            </form>
 | 
					 | 
				
			||||||
            `);
 | 
					 | 
				
			||||||
        navigator.credentials
 | 
					 | 
				
			||||||
            .get({
 | 
					 | 
				
			||||||
                publicKey: this.transformCredentialRequestOptions(
 | 
					 | 
				
			||||||
                    this.deviceChallenge?.challenge as PublicKeyCredentialRequestOptions,
 | 
					 | 
				
			||||||
                ),
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
            .then((assertion) => {
 | 
					 | 
				
			||||||
                if (!assertion) {
 | 
					 | 
				
			||||||
                    throw new Error("No assertion");
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                try {
 | 
					 | 
				
			||||||
                    // we now have an authentication assertion! encode the byte arrays contained
 | 
					 | 
				
			||||||
                    // in the assertion data as strings for posting to the server
 | 
					 | 
				
			||||||
                    const transformedAssertionForServer = this.transformAssertionForServer(
 | 
					 | 
				
			||||||
                        assertion as PublicKeyCredential,
 | 
					 | 
				
			||||||
                    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    // post the assertion to the server for verification.
 | 
					 | 
				
			||||||
                    this.executor.submit({
 | 
					 | 
				
			||||||
                        webauthn: transformedAssertionForServer,
 | 
					 | 
				
			||||||
                    });
 | 
					 | 
				
			||||||
                } catch (err) {
 | 
					 | 
				
			||||||
                    throw new Error(`Error when validating assertion on server: ${err}`);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
            .catch((error) => {
 | 
					 | 
				
			||||||
                console.warn(error);
 | 
					 | 
				
			||||||
                this.deviceChallenge = undefined;
 | 
					 | 
				
			||||||
                this.render();
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const sfe = new SimpleFlowExecutor($("#flow-sfe-container")[0] as HTMLDivElement);
 | 
					 | 
				
			||||||
sfe.start();
 | 
					 | 
				
			||||||
@ -1,7 +0,0 @@
 | 
				
			|||||||
{
 | 
					 | 
				
			||||||
    "compilerOptions": {
 | 
					 | 
				
			||||||
        "types": ["jquery"],
 | 
					 | 
				
			||||||
        "esModuleInterop": true,
 | 
					 | 
				
			||||||
        "lib": ["DOM", "ES2015", "ES2017"]
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										25
									
								
								web/paths.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								web/paths.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @file Path constants for the web package.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					import { dirname, resolve } from "node:path";
 | 
				
			||||||
 | 
					import { fileURLToPath } from "node:url";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const __dirname = dirname(fileURLToPath(import.meta.url));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @typedef {'@goauthentik/web'} WebPackageIdentifier
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * The root of the web package.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export const PackageRoot = /** @type {WebPackageIdentifier} */ (resolve(__dirname));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Path to the web package's distribution directory.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * This is where the built files are located after running the build process.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export const DistDirectory = /** @type {`${WebPackageIdentifier}/dist`} */ (
 | 
				
			||||||
 | 
					    resolve(__dirname, "dist")
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
							
								
								
									
										90
									
								
								web/scripts/build-sfe.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								web/scripts/build-sfe.mjs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,90 @@
 | 
				
			|||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @file Build script for the simplified flow executor (SFE).
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					import { DistDirectory, PackageRoot } from "@goauthentik/web/paths";
 | 
				
			||||||
 | 
					import esbuild from "esbuild";
 | 
				
			||||||
 | 
					import copy from "esbuild-plugin-copy";
 | 
				
			||||||
 | 
					import { es5Plugin } from "esbuild-plugin-es5";
 | 
				
			||||||
 | 
					import { createRequire } from "node:module";
 | 
				
			||||||
 | 
					import * as path from "node:path";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Builds the Simplified Flow Executor bundle.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @remarks
 | 
				
			||||||
 | 
					 * The output directory and file names are referenced by the backend.
 | 
				
			||||||
 | 
					 * @see {@link ../../authentik/flows/templates/if/flow-sfe.html}
 | 
				
			||||||
 | 
					 * @returns {Promise<void>}
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					async function buildSFE() {
 | 
				
			||||||
 | 
					    const require = createRequire(import.meta.url);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const sourceDirectory = path.join(PackageRoot, "sfe");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const entryPoint = path.join(sourceDirectory, "main.js");
 | 
				
			||||||
 | 
					    const outDirectory = path.join(DistDirectory, "sfe");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const bootstrapCSSPath = require.resolve(
 | 
				
			||||||
 | 
					        path.join("bootstrap", "dist", "css", "bootstrap.min.css"),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @type {esbuild.BuildOptions}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    const config = {
 | 
				
			||||||
 | 
					        tsconfig: path.join(sourceDirectory, "tsconfig.json"),
 | 
				
			||||||
 | 
					        entryPoints: [entryPoint],
 | 
				
			||||||
 | 
					        minify: false,
 | 
				
			||||||
 | 
					        bundle: true,
 | 
				
			||||||
 | 
					        sourcemap: true,
 | 
				
			||||||
 | 
					        treeShaking: true,
 | 
				
			||||||
 | 
					        legalComments: "external",
 | 
				
			||||||
 | 
					        platform: "browser",
 | 
				
			||||||
 | 
					        format: "iife",
 | 
				
			||||||
 | 
					        alias: {
 | 
				
			||||||
 | 
					            "@swc/helpers": path.dirname(require.resolve("@swc/helpers/package.json")),
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        banner: {
 | 
				
			||||||
 | 
					            js: [
 | 
				
			||||||
 | 
					                // ---
 | 
				
			||||||
 | 
					                "// Simplified Flow Executor (SFE)",
 | 
				
			||||||
 | 
					                `// Bundled on ${new Date().toISOString()}`,
 | 
				
			||||||
 | 
					                "// @ts-nocheck",
 | 
				
			||||||
 | 
					                "",
 | 
				
			||||||
 | 
					            ].join("\n"),
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        plugins: [
 | 
				
			||||||
 | 
					            copy({
 | 
				
			||||||
 | 
					                assets: [
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        from: bootstrapCSSPath,
 | 
				
			||||||
 | 
					                        to: outDirectory,
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
 | 
					            es5Plugin({
 | 
				
			||||||
 | 
					                swc: {
 | 
				
			||||||
 | 
					                    jsc: {
 | 
				
			||||||
 | 
					                        loose: false,
 | 
				
			||||||
 | 
					                        externalHelpers: false,
 | 
				
			||||||
 | 
					                        keepClassNames: false,
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                    minify: false,
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        target: ["es5"],
 | 
				
			||||||
 | 
					        outdir: outDirectory,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    esbuild.build(config);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					buildSFE()
 | 
				
			||||||
 | 
					    .then(() => {
 | 
				
			||||||
 | 
					        console.log("Build complete");
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .catch((error) => {
 | 
				
			||||||
 | 
					        console.error("Build failed", error);
 | 
				
			||||||
 | 
					        process.exit(1);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
@ -1,3 +1,4 @@
 | 
				
			|||||||
 | 
					import { DistDirectory, PackageRoot } from "@goauthentik/web/paths";
 | 
				
			||||||
import { execFileSync } from "child_process";
 | 
					import { execFileSync } from "child_process";
 | 
				
			||||||
import { deepmerge } from "deepmerge-ts";
 | 
					import { deepmerge } from "deepmerge-ts";
 | 
				
			||||||
import esbuild from "esbuild";
 | 
					import esbuild from "esbuild";
 | 
				
			||||||
@ -170,7 +171,7 @@ function composeVersionID() {
 | 
				
			|||||||
 * @throws {Error} on build failure
 | 
					 * @throws {Error} on build failure
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
function createEntryPointOptions([source, dest], overrides = {}) {
 | 
					function createEntryPointOptions([source, dest], overrides = {}) {
 | 
				
			||||||
    const outdir = path.join(__dirname, "..", "dist", dest);
 | 
					    const outdir = path.join(DistDirectory, dest);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * @type {esbuild.BuildOptions}
 | 
					     * @type {esbuild.BuildOptions}
 | 
				
			||||||
@ -233,7 +234,7 @@ async function doWatch() {
 | 
				
			|||||||
                        buildObserverPlugin({
 | 
					                        buildObserverPlugin({
 | 
				
			||||||
                            serverURL,
 | 
					                            serverURL,
 | 
				
			||||||
                            logPrefix: entryPoint[1],
 | 
					                            logPrefix: entryPoint[1],
 | 
				
			||||||
                            relativeRoot: path.join(__dirname, ".."),
 | 
					                            relativeRoot: PackageRoot,
 | 
				
			||||||
                        }),
 | 
					                        }),
 | 
				
			||||||
                    ],
 | 
					                    ],
 | 
				
			||||||
                    define: {
 | 
					                    define: {
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										191
									
								
								web/sfe/lib/AuthenticatorValidateStage.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										191
									
								
								web/sfe/lib/AuthenticatorValidateStage.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,191 @@
 | 
				
			|||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @import { AuthenticatorValidationChallenge, DeviceChallenge } from "@goauthentik/api";
 | 
				
			||||||
 | 
					 * @import { FlowExecutor } from './Stage.js';
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					    isWebAuthnSupported,
 | 
				
			||||||
 | 
					    transformAssertionForServer,
 | 
				
			||||||
 | 
					    transformCredentialRequestOptions,
 | 
				
			||||||
 | 
					} from "@goauthentik/web/authentication";
 | 
				
			||||||
 | 
					import $ from "jquery";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { Stage } from "./Stage.js";
 | 
				
			||||||
 | 
					import { ak } from "./utils.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//@ts-check
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @template {AuthenticatorValidationChallenge} T
 | 
				
			||||||
 | 
					 * @extends {Stage<T>}
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export class AuthenticatorValidateStage extends Stage {
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @param {FlowExecutor} executor - The executor for this stage
 | 
				
			||||||
 | 
					     * @param {T} challenge - The challenge for this stage
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    constructor(executor, challenge) {
 | 
				
			||||||
 | 
					        super(executor, challenge);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					         * @type {DeviceChallenge | null}
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        this.deviceChallenge = null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    render() {
 | 
				
			||||||
 | 
					        if (!this.deviceChallenge) {
 | 
				
			||||||
 | 
					            this.renderChallengePicker();
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        switch (this.deviceChallenge.deviceClass) {
 | 
				
			||||||
 | 
					            case "static":
 | 
				
			||||||
 | 
					            case "totp":
 | 
				
			||||||
 | 
					                this.renderCodeInput();
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					            case "webauthn":
 | 
				
			||||||
 | 
					                this.renderWebauthn();
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					            default:
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @private
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    renderChallengePicker() {
 | 
				
			||||||
 | 
					        const challenges = this.challenge.deviceChallenges.filter((challenge) =>
 | 
				
			||||||
 | 
					            challenge.deviceClass === "webauthn" && !isWebAuthnSupported() ? undefined : challenge,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.html(/* html */ `<form id="picker-form">
 | 
				
			||||||
 | 
					                <img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
 | 
				
			||||||
 | 
					                <h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
 | 
				
			||||||
 | 
					                ${
 | 
				
			||||||
 | 
					                    challenges.length > 0
 | 
				
			||||||
 | 
					                        ? /* html */ `<p>Select an authentication method.</p>`
 | 
				
			||||||
 | 
					                        : /* html */ `<p>No compatible authentication method available</p>`
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                ${challenges
 | 
				
			||||||
 | 
					                    .map((challenge) => {
 | 
				
			||||||
 | 
					                        let label = undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        switch (challenge.deviceClass) {
 | 
				
			||||||
 | 
					                            case "static":
 | 
				
			||||||
 | 
					                                label = "Recovery keys";
 | 
				
			||||||
 | 
					                                break;
 | 
				
			||||||
 | 
					                            case "totp":
 | 
				
			||||||
 | 
					                                label = "Traditional authenticator";
 | 
				
			||||||
 | 
					                                break;
 | 
				
			||||||
 | 
					                            case "webauthn":
 | 
				
			||||||
 | 
					                                label = "Security key";
 | 
				
			||||||
 | 
					                                break;
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        if (!label) return "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        return /* html */ `<div class="form-label-group my-3 has-validation">
 | 
				
			||||||
 | 
					                            <button id="${challenge.deviceClass}-${challenge.deviceUid}" class="btn btn-secondary w-100 py-2" type="button">
 | 
				
			||||||
 | 
					                                ${label}
 | 
				
			||||||
 | 
					                            </button>
 | 
				
			||||||
 | 
					                        </div>`;
 | 
				
			||||||
 | 
					                    })
 | 
				
			||||||
 | 
					                    .join("")}
 | 
				
			||||||
 | 
					            </form>`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.challenge.deviceChallenges.forEach((challenge) => {
 | 
				
			||||||
 | 
					            $(`#picker-form button#${challenge.deviceClass}-${challenge.deviceUid}`).on(
 | 
				
			||||||
 | 
					                "click",
 | 
				
			||||||
 | 
					                () => {
 | 
				
			||||||
 | 
					                    this.deviceChallenge = challenge;
 | 
				
			||||||
 | 
					                    this.render();
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @private
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    renderCodeInput() {
 | 
				
			||||||
 | 
					        this.html(/* html */ `
 | 
				
			||||||
 | 
					            <form id="totp-form">
 | 
				
			||||||
 | 
					                <img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
 | 
				
			||||||
 | 
					                <h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
 | 
				
			||||||
 | 
					                <div class="form-label-group my-3 has-validation">
 | 
				
			||||||
 | 
					                    <input type="text" autofocus class="form-control ${this.error("code").length > 0 ? "is-invalid" : ""}" name="code" placeholder="Please enter your code" autocomplete="one-time-code">
 | 
				
			||||||
 | 
					                    ${this.renderInputError("code")}
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                <button class="btn btn-primary w-100 py-2" type="submit">Continue</button>
 | 
				
			||||||
 | 
					            </form>`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $("#totp-form input").trigger("focus");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $("#totp-form").on("submit", (ev) => {
 | 
				
			||||||
 | 
					            ev.preventDefault();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const target = /** @type {HTMLFormElement} */ (ev.target);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const data = new FormData(target);
 | 
				
			||||||
 | 
					            this.executor.submit(data);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @private
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    renderWebauthn() {
 | 
				
			||||||
 | 
					        this.html(/* html */ `
 | 
				
			||||||
 | 
					            <form id="totp-form">
 | 
				
			||||||
 | 
					                <img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
 | 
				
			||||||
 | 
					                <h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
 | 
				
			||||||
 | 
					                <div class="d-flex justify-content-center">
 | 
				
			||||||
 | 
					                    <div class="spinner-border" role="status">
 | 
				
			||||||
 | 
					                        <span class="sr-only">Loading...</span>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            </form>
 | 
				
			||||||
 | 
					            `);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const challenge = /** @type {PublicKeyCredentialRequestOptions} */ (
 | 
				
			||||||
 | 
					            this.deviceChallenge?.challenge
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        navigator.credentials
 | 
				
			||||||
 | 
					            .get({
 | 
				
			||||||
 | 
					                publicKey: transformCredentialRequestOptions(challenge),
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .then((credential) => {
 | 
				
			||||||
 | 
					                if (!credential) {
 | 
				
			||||||
 | 
					                    throw new Error("No assertion");
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (credential.type !== "public-key") {
 | 
				
			||||||
 | 
					                    throw new Error("Invalid assertion type");
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                try {
 | 
				
			||||||
 | 
					                    // We now have an authentication assertion!
 | 
				
			||||||
 | 
					                    // Encode the byte arrays contained in the assertion data as strings
 | 
				
			||||||
 | 
					                    // for posting to the server.
 | 
				
			||||||
 | 
					                    const transformedAssertionForServer = transformAssertionForServer(
 | 
				
			||||||
 | 
					                        /** @type {PublicKeyCredential} */ (credential),
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    // Post the assertion to the server for verification.
 | 
				
			||||||
 | 
					                    this.executor.submit({
 | 
				
			||||||
 | 
					                        webauthn: transformedAssertionForServer,
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                } catch (err) {
 | 
				
			||||||
 | 
					                    throw new Error(`Error when validating assertion on server: ${err}`);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .catch((error) => {
 | 
				
			||||||
 | 
					                console.warn(error);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                this.deviceChallenge = null;
 | 
				
			||||||
 | 
					                this.render();
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										35
									
								
								web/sfe/lib/AutosubmitStage.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								web/sfe/lib/AutosubmitStage.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,35 @@
 | 
				
			|||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @import { AutosubmitChallenge } from "@goauthentik/api";
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					import $ from "jquery";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { Stage } from "./Stage.js";
 | 
				
			||||||
 | 
					import { ak } from "./utils.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @template {AutosubmitChallenge} T
 | 
				
			||||||
 | 
					 * @extends {Stage<T>}
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export class AutosubmitStage extends Stage {
 | 
				
			||||||
 | 
					    render() {
 | 
				
			||||||
 | 
					        this.html(/* html */ `
 | 
				
			||||||
 | 
					            <form id="autosubmit-form" action="${this.challenge.url}" method="POST">
 | 
				
			||||||
 | 
					                <img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
 | 
				
			||||||
 | 
					                <h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
 | 
				
			||||||
 | 
					                ${Object.entries(this.challenge.attrs).map(([key, value]) => {
 | 
				
			||||||
 | 
					                    return /* html */ `<input
 | 
				
			||||||
 | 
					                            type="hidden"
 | 
				
			||||||
 | 
					                            name="${key}"
 | 
				
			||||||
 | 
					                            value="${value}"
 | 
				
			||||||
 | 
					                        />`;
 | 
				
			||||||
 | 
					                })}
 | 
				
			||||||
 | 
					                <div class="d-flex justify-content-center">
 | 
				
			||||||
 | 
					                    <div class="spinner-border" role="status">
 | 
				
			||||||
 | 
					                        <span class="sr-only">Loading...</span>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            </form>`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $("#autosubmit-form").submit();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										50
									
								
								web/sfe/lib/IdentificationStage.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								web/sfe/lib/IdentificationStage.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,50 @@
 | 
				
			|||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @import { IdentificationChallenge } from "@goauthentik/api";
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					import $ from "jquery";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { Stage } from "./Stage.js";
 | 
				
			||||||
 | 
					import { ak } from "./utils.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @template {IdentificationChallenge} T
 | 
				
			||||||
 | 
					 * @extends {Stage<T>}
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export class IdentificationStage extends Stage {
 | 
				
			||||||
 | 
					    render() {
 | 
				
			||||||
 | 
					        this.html(/* html */ `
 | 
				
			||||||
 | 
					            <form id="ident-form">
 | 
				
			||||||
 | 
					                <img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
 | 
				
			||||||
 | 
					                <h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
 | 
				
			||||||
 | 
					                ${
 | 
				
			||||||
 | 
					                    this.challenge.applicationPre
 | 
				
			||||||
 | 
					                        ? /* html */ `<p>
 | 
				
			||||||
 | 
					                              Log in to continue to ${this.challenge.applicationPre}.
 | 
				
			||||||
 | 
					                          </p>`
 | 
				
			||||||
 | 
					                        : ""
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                <div class="form-label-group my-3 has-validation">
 | 
				
			||||||
 | 
					                    <input type="text" autofocus class="form-control" name="uid_field" placeholder="Email / Username">
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                ${
 | 
				
			||||||
 | 
					                    this.challenge.passwordFields
 | 
				
			||||||
 | 
					                        ? /* html */ `<div class="form-label-group my-3 has-validation">
 | 
				
			||||||
 | 
					                                <input type="password" class="form-control ${this.error("password").length > 0 ? "is-invalid" : ""}" name="password" placeholder="Password">
 | 
				
			||||||
 | 
					                                ${this.renderInputError("password")}
 | 
				
			||||||
 | 
					                        </div>`
 | 
				
			||||||
 | 
					                        : ""
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                ${this.renderNonFieldErrors()}
 | 
				
			||||||
 | 
					                <button class="btn btn-primary w-100 py-2" type="submit">${this.challenge.primaryAction}</button>
 | 
				
			||||||
 | 
					            </form>`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $("#ident-form input[name=uid_field]").trigger("focus");
 | 
				
			||||||
 | 
					        $("#ident-form").on("submit", (ev) => {
 | 
				
			||||||
 | 
					            ev.preventDefault();
 | 
				
			||||||
 | 
					            const target = /** @type {HTMLFormElement} */ (ev.target);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const data = new FormData(target);
 | 
				
			||||||
 | 
					            this.executor.submit(data);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										37
									
								
								web/sfe/lib/PasswordStage.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								web/sfe/lib/PasswordStage.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,37 @@
 | 
				
			|||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @import { PasswordChallenge } from "@goauthentik/api";
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					import $ from "jquery";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { Stage } from "./Stage.js";
 | 
				
			||||||
 | 
					import { ak } from "./utils.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @template {PasswordChallenge} T
 | 
				
			||||||
 | 
					 * @extends {Stage<T>}
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export class PasswordStage extends Stage {
 | 
				
			||||||
 | 
					    render() {
 | 
				
			||||||
 | 
					        this.html(/* html */ `
 | 
				
			||||||
 | 
					            <form id="password-form">
 | 
				
			||||||
 | 
					                <img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
 | 
				
			||||||
 | 
					                <h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
 | 
				
			||||||
 | 
					                <div class="form-label-group my-3 has-validation">
 | 
				
			||||||
 | 
					                    <input type="password" autofocus class="form-control ${this.error("password").length > 0 ? "is-invalid" : ""}" name="password" placeholder="Password">
 | 
				
			||||||
 | 
					                    ${this.renderInputError("password")}
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                <button class="btn btn-primary w-100 py-2" type="submit">Continue</button>
 | 
				
			||||||
 | 
					            </form>`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $("#password-form input").trigger("focus");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $("#password-form").on("submit", (ev) => {
 | 
				
			||||||
 | 
					            ev.preventDefault();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const target = /** @type {HTMLFormElement} */ (ev.target);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const data = new FormData(target);
 | 
				
			||||||
 | 
					            this.executor.submit(data);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										14
									
								
								web/sfe/lib/RedirectStage.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								web/sfe/lib/RedirectStage.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,14 @@
 | 
				
			|||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @import { RedirectChallenge } from "@goauthentik/api";
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					import { Stage } from "./Stage.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @template {RedirectChallenge} T
 | 
				
			||||||
 | 
					 * @extends {Stage<T>}
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export class RedirectStage extends Stage {
 | 
				
			||||||
 | 
					    render() {
 | 
				
			||||||
 | 
					        window.location.assign(this.challenge.to);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										113
									
								
								web/sfe/lib/SimpleFlowExecutor.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								web/sfe/lib/SimpleFlowExecutor.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,113 @@
 | 
				
			|||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @import { ChallengeTypes } from "@goauthentik/api";
 | 
				
			||||||
 | 
					 * @import { FlowExecutor } from './Stage.js';
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					import $ from "jquery";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { ChallengeTypesFromJSON } from "@goauthentik/api";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { AuthenticatorValidateStage } from "./AuthenticatorValidateStage.js";
 | 
				
			||||||
 | 
					import { AutosubmitStage } from "./AutosubmitStage.js";
 | 
				
			||||||
 | 
					import { IdentificationStage } from "./IdentificationStage.js";
 | 
				
			||||||
 | 
					import { PasswordStage } from "./PasswordStage.js";
 | 
				
			||||||
 | 
					import { RedirectStage } from "./RedirectStage.js";
 | 
				
			||||||
 | 
					import { ak } from "./utils.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Simple Flow Executor lifecycle.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @implements {FlowExecutor}
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export class SimpleFlowExecutor {
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param {HTMLDivElement} container
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    constructor(container) {
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					         * @type {ChallengeTypes | null} The current challenge.
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        this.challenge = null;
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					         * @type {string} The flow slug.
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        this.flowSlug = window.location.pathname.split("/")[3] || "";
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					         * @type {HTMLDivElement} The container element for the flow executor.
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        this.container = container;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    get apiURL() {
 | 
				
			||||||
 | 
					        return `${ak().api.base}api/v3/flows/executor/${this.flowSlug}/?query=${encodeURIComponent(window.location.search.substring(1))}`;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    start() {
 | 
				
			||||||
 | 
					        $.ajax({
 | 
				
			||||||
 | 
					            type: "GET",
 | 
				
			||||||
 | 
					            url: this.apiURL,
 | 
				
			||||||
 | 
					            success: (data) => {
 | 
				
			||||||
 | 
					                this.challenge = ChallengeTypesFromJSON(data);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                this.renderChallenge();
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Submits the form data.
 | 
				
			||||||
 | 
					     * @param {Record<string, unknown> | FormData} payload
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    submit(payload) {
 | 
				
			||||||
 | 
					        $("button[type=submit]").addClass("disabled")
 | 
				
			||||||
 | 
					            .html(`<span class="spinner-border spinner-border-sm" aria-hidden="true"></span>
 | 
				
			||||||
 | 
					                <span role="status">Loading...</span>`);
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					         * @type {Record<string, unknown>}
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        let finalData;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (payload instanceof FormData) {
 | 
				
			||||||
 | 
					            finalData = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            payload.forEach((value, key) => {
 | 
				
			||||||
 | 
					                finalData[key] = value;
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            finalData = payload;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $.ajax({
 | 
				
			||||||
 | 
					            type: "POST",
 | 
				
			||||||
 | 
					            url: this.apiURL,
 | 
				
			||||||
 | 
					            data: JSON.stringify(finalData),
 | 
				
			||||||
 | 
					            success: (data) => {
 | 
				
			||||||
 | 
					                this.challenge = ChallengeTypesFromJSON(data);
 | 
				
			||||||
 | 
					                this.renderChallenge();
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            contentType: "application/json",
 | 
				
			||||||
 | 
					            dataType: "json",
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @returns {void}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    renderChallenge() {
 | 
				
			||||||
 | 
					        switch (this.challenge?.component) {
 | 
				
			||||||
 | 
					            case "ak-stage-identification":
 | 
				
			||||||
 | 
					                return new IdentificationStage(this, this.challenge).render();
 | 
				
			||||||
 | 
					            case "ak-stage-password":
 | 
				
			||||||
 | 
					                return new PasswordStage(this, this.challenge).render();
 | 
				
			||||||
 | 
					            case "xak-flow-redirect":
 | 
				
			||||||
 | 
					                return new RedirectStage(this, this.challenge).render();
 | 
				
			||||||
 | 
					            case "ak-stage-autosubmit":
 | 
				
			||||||
 | 
					                return new AutosubmitStage(this, this.challenge).render();
 | 
				
			||||||
 | 
					            case "ak-stage-authenticator-validate":
 | 
				
			||||||
 | 
					                return new AuthenticatorValidateStage(this, this.challenge).render();
 | 
				
			||||||
 | 
					            default:
 | 
				
			||||||
 | 
					                this.container.innerText = `Unsupported stage: ${this.challenge?.component}`;
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										116
									
								
								web/sfe/lib/Stage.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								web/sfe/lib/Stage.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,116 @@
 | 
				
			|||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @import { ContextualFlowInfo, ErrorDetail } from "@goauthentik/api";
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @typedef {object} FlowInfoChallenge
 | 
				
			||||||
 | 
					 * @property {ContextualFlowInfo} [flowInfo]
 | 
				
			||||||
 | 
					 * @property {Record<string, Array<ErrorDetail>>} [responseErrors]
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @abstract
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export class FlowExecutor {
 | 
				
			||||||
 | 
					    constructor() {
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					         * The DOM container element.
 | 
				
			||||||
 | 
					         *
 | 
				
			||||||
 | 
					         * @type {HTMLElement}
 | 
				
			||||||
 | 
					         * @abstract
 | 
				
			||||||
 | 
					         * @returns {void}
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        // eslint-disable-next-line @typescript-eslint/no-unused-expressions
 | 
				
			||||||
 | 
					        this.container;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Submits the form data.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param {Record<string, unknown> | FormData} data The data to submit.
 | 
				
			||||||
 | 
					     * @abstract
 | 
				
			||||||
 | 
					     * @returns {void}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
				
			||||||
 | 
					    submit(data) {
 | 
				
			||||||
 | 
					        throw new Error(`Method 'submit' not implemented in ${this.constructor.name}`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Represents a stage in a flow
 | 
				
			||||||
 | 
					 * @template {FlowInfoChallenge} T
 | 
				
			||||||
 | 
					 * @abstract
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export class Stage {
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @param {FlowExecutor} executor - The executor for this stage
 | 
				
			||||||
 | 
					     * @param {T} challenge - The challenge for this stage
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    constructor(executor, challenge) {
 | 
				
			||||||
 | 
					        /** @type {FlowExecutor} */
 | 
				
			||||||
 | 
					        this.executor = executor;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /** @type {T} */
 | 
				
			||||||
 | 
					        this.challenge = challenge;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @protected
 | 
				
			||||||
 | 
					     * @param {string} fieldName
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    error(fieldName) {
 | 
				
			||||||
 | 
					        if (!this.challenge.responseErrors) {
 | 
				
			||||||
 | 
					            return [];
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return this.challenge.responseErrors[fieldName] || [];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @protected
 | 
				
			||||||
 | 
					     * @param {string} fieldName
 | 
				
			||||||
 | 
					     * @returns {string}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    renderInputError(fieldName) {
 | 
				
			||||||
 | 
					        return `${this.error(fieldName)
 | 
				
			||||||
 | 
					            .map((error) => {
 | 
				
			||||||
 | 
					                return /* html */ `<div class="invalid-feedback">
 | 
				
			||||||
 | 
					                    ${error.string}
 | 
				
			||||||
 | 
					                </div>`;
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .join("")}`;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @protected
 | 
				
			||||||
 | 
					     * @returns {string}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    renderNonFieldErrors() {
 | 
				
			||||||
 | 
					        return `${this.error("non_field_errors")
 | 
				
			||||||
 | 
					            .map((error) => {
 | 
				
			||||||
 | 
					                return /* html */ `<div class="alert alert-danger" role="alert">
 | 
				
			||||||
 | 
					                    ${error.string}
 | 
				
			||||||
 | 
					                </div>`;
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .join("")}`;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @protected
 | 
				
			||||||
 | 
					     * @param {string} innerHTML
 | 
				
			||||||
 | 
					     * @returns {void}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    html(innerHTML) {
 | 
				
			||||||
 | 
					        this.executor.container.innerHTML = innerHTML;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Renders the stage (must be implemented by subclasses)
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @abstract
 | 
				
			||||||
 | 
					     * @returns {void}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    render() {
 | 
				
			||||||
 | 
					        throw new Error("Abstract method");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										12
									
								
								web/sfe/lib/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								web/sfe/lib/index.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @file Simplified Flow Executor (SFE) library module.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export * from "./Stage.js";
 | 
				
			||||||
 | 
					export * from "./SimpleFlowExecutor.js";
 | 
				
			||||||
 | 
					export * from "./AuthenticatorValidateStage.js";
 | 
				
			||||||
 | 
					export * from "./AutosubmitStage.js";
 | 
				
			||||||
 | 
					export * from "./IdentificationStage.js";
 | 
				
			||||||
 | 
					export * from "./PasswordStage.js";
 | 
				
			||||||
 | 
					export * from "./RedirectStage.js";
 | 
				
			||||||
 | 
					export * from "./utils.js";
 | 
				
			||||||
							
								
								
									
										20
									
								
								web/sfe/lib/utils.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								web/sfe/lib/utils.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,20 @@
 | 
				
			|||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @typedef {object} GlobalAuthentik
 | 
				
			||||||
 | 
					 * @property {object} brand
 | 
				
			||||||
 | 
					 * @property {string} brand.branding_logo
 | 
				
			||||||
 | 
					 * @property {object} api
 | 
				
			||||||
 | 
					 * @property {string} api.base
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Retrieves the global authentik object from the window.
 | 
				
			||||||
 | 
					 * @throws {Error} If the object not found
 | 
				
			||||||
 | 
					 * @returns {GlobalAuthentik}
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export function ak() {
 | 
				
			||||||
 | 
					    if (!("authentik" in window)) {
 | 
				
			||||||
 | 
					        throw new Error("No authentik object found in window");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return /** @type {GlobalAuthentik} */ (window.authentik);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										17
									
								
								web/sfe/main.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								web/sfe/main.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @file Simplified Flow Executor (SFE) entry point.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					import "formdata-polyfill";
 | 
				
			||||||
 | 
					import $ from "jquery";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { SimpleFlowExecutor } from "./lib/index.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const flowContainer = /** @type {HTMLDivElement} */ ($("#flow-sfe-container")[0]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if (!flowContainer) {
 | 
				
			||||||
 | 
					    throw new Error("No flow container element found");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const sfe = new SimpleFlowExecutor(flowContainer);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					sfe.start();
 | 
				
			||||||
							
								
								
									
										46
									
								
								web/sfe/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								web/sfe/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,46 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					    // TODO: Replace with @goauthentik/tsconfig after project compilation.
 | 
				
			||||||
 | 
					    "$schema": "https://json.schemastore.org/tsconfig",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    "compilerOptions": {
 | 
				
			||||||
 | 
					        "paths": {
 | 
				
			||||||
 | 
					            "@goauthentik/web/authentication": ["../authentication/index.js"]
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "alwaysStrict": true,
 | 
				
			||||||
 | 
					        "baseUrl": ".",
 | 
				
			||||||
 | 
					        "rootDir": "../",
 | 
				
			||||||
 | 
					        "composite": true,
 | 
				
			||||||
 | 
					        "declaration": true,
 | 
				
			||||||
 | 
					        "allowJs": true,
 | 
				
			||||||
 | 
					        "declarationMap": true,
 | 
				
			||||||
 | 
					        "isolatedModules": true,
 | 
				
			||||||
 | 
					        "incremental": true,
 | 
				
			||||||
 | 
					        "emitDeclarationOnly": true,
 | 
				
			||||||
 | 
					        "esModuleInterop": true,
 | 
				
			||||||
 | 
					        "lib": ["DOM", "ES2015", "ES2017"],
 | 
				
			||||||
 | 
					        "module": "NodeNext",
 | 
				
			||||||
 | 
					        "moduleResolution": "NodeNext",
 | 
				
			||||||
 | 
					        "newLine": "lf",
 | 
				
			||||||
 | 
					        "noFallthroughCasesInSwitch": true,
 | 
				
			||||||
 | 
					        "noImplicitOverride": false,
 | 
				
			||||||
 | 
					        "outDir": "${configDir}/out",
 | 
				
			||||||
 | 
					        "pretty": true,
 | 
				
			||||||
 | 
					        "skipDefaultLibCheck": true,
 | 
				
			||||||
 | 
					        "skipLibCheck": true,
 | 
				
			||||||
 | 
					        "sourceMap": true,
 | 
				
			||||||
 | 
					        "strict": true,
 | 
				
			||||||
 | 
					        "noUncheckedIndexedAccess": true,
 | 
				
			||||||
 | 
					        "target": "ESNext",
 | 
				
			||||||
 | 
					        "useUnknownInCatchVariables": true
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "exclude": [
 | 
				
			||||||
 | 
					        // ---
 | 
				
			||||||
 | 
					        "./out/**/*",
 | 
				
			||||||
 | 
					        "./dist/**/*"
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    "include": [
 | 
				
			||||||
 | 
					        // ---
 | 
				
			||||||
 | 
					        "./**/*.js",
 | 
				
			||||||
 | 
					        "../authentication/**/*.js"
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,145 +0,0 @@
 | 
				
			|||||||
import * as base64js from "base64-js";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { msg } from "@lit/localize";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function b64enc(buf: Uint8Array): string {
 | 
					 | 
				
			||||||
    return base64js.fromByteArray(buf).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function b64RawEnc(buf: Uint8Array): string {
 | 
					 | 
				
			||||||
    return base64js.fromByteArray(buf).replace(/\+/g, "-").replace(/\//g, "_");
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function u8arr(input: string): Uint8Array {
 | 
					 | 
				
			||||||
    return Uint8Array.from(atob(input.replace(/_/g, "/").replace(/-/g, "+")), (c) =>
 | 
					 | 
				
			||||||
        c.charCodeAt(0),
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function checkWebAuthnSupport() {
 | 
					 | 
				
			||||||
    if ("credentials" in navigator) {
 | 
					 | 
				
			||||||
        return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (window.location.protocol === "http:" && window.location.hostname !== "localhost") {
 | 
					 | 
				
			||||||
        throw new Error(msg("WebAuthn requires this page to be accessed via HTTPS."));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    throw new Error(msg("WebAuthn not supported by browser."));
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * Transforms items in the credentialCreateOptions generated on the server
 | 
					 | 
				
			||||||
 * into byte arrays expected by the navigator.credentials.create() call
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
export function transformCredentialCreateOptions(
 | 
					 | 
				
			||||||
    credentialCreateOptions: PublicKeyCredentialCreationOptions,
 | 
					 | 
				
			||||||
    userId: string,
 | 
					 | 
				
			||||||
): PublicKeyCredentialCreationOptions {
 | 
					 | 
				
			||||||
    const user = credentialCreateOptions.user;
 | 
					 | 
				
			||||||
    // Because json can't contain raw bytes, the server base64-encodes the User ID
 | 
					 | 
				
			||||||
    // So to get the base64 encoded byte array, we first need to convert it to a regular
 | 
					 | 
				
			||||||
    // string, then a byte array, re-encode it and wrap that in an array.
 | 
					 | 
				
			||||||
    const stringId = decodeURIComponent(window.atob(userId));
 | 
					 | 
				
			||||||
    user.id = u8arr(b64enc(u8arr(stringId)));
 | 
					 | 
				
			||||||
    const challenge = u8arr(credentialCreateOptions.challenge.toString());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
        ...credentialCreateOptions,
 | 
					 | 
				
			||||||
        challenge,
 | 
					 | 
				
			||||||
        user,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface Assertion {
 | 
					 | 
				
			||||||
    id: string;
 | 
					 | 
				
			||||||
    rawId: string;
 | 
					 | 
				
			||||||
    type: string;
 | 
					 | 
				
			||||||
    registrationClientExtensions: string;
 | 
					 | 
				
			||||||
    response: {
 | 
					 | 
				
			||||||
        clientDataJSON: string;
 | 
					 | 
				
			||||||
        attestationObject: string;
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * Transforms the binary data in the credential into base64 strings
 | 
					 | 
				
			||||||
 * for posting to the server.
 | 
					 | 
				
			||||||
 * @param {PublicKeyCredential} newAssertion
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
export function transformNewAssertionForServer(newAssertion: PublicKeyCredential): Assertion {
 | 
					 | 
				
			||||||
    const attObj = new Uint8Array(
 | 
					 | 
				
			||||||
        (newAssertion.response as AuthenticatorAttestationResponse).attestationObject,
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
    const clientDataJSON = new Uint8Array(newAssertion.response.clientDataJSON);
 | 
					 | 
				
			||||||
    const rawId = new Uint8Array(newAssertion.rawId);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const registrationClientExtensions = newAssertion.getClientExtensionResults();
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
        id: newAssertion.id,
 | 
					 | 
				
			||||||
        rawId: b64enc(rawId),
 | 
					 | 
				
			||||||
        type: newAssertion.type,
 | 
					 | 
				
			||||||
        registrationClientExtensions: JSON.stringify(registrationClientExtensions),
 | 
					 | 
				
			||||||
        response: {
 | 
					 | 
				
			||||||
            clientDataJSON: b64enc(clientDataJSON),
 | 
					 | 
				
			||||||
            attestationObject: b64enc(attObj),
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function transformCredentialRequestOptions(
 | 
					 | 
				
			||||||
    credentialRequestOptions: PublicKeyCredentialRequestOptions,
 | 
					 | 
				
			||||||
): PublicKeyCredentialRequestOptions {
 | 
					 | 
				
			||||||
    const challenge = u8arr(credentialRequestOptions.challenge.toString());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const allowCredentials = (credentialRequestOptions.allowCredentials || []).map(
 | 
					 | 
				
			||||||
        (credentialDescriptor) => {
 | 
					 | 
				
			||||||
            const id = u8arr(credentialDescriptor.id.toString());
 | 
					 | 
				
			||||||
            return Object.assign({}, credentialDescriptor, { id });
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
        ...credentialRequestOptions,
 | 
					 | 
				
			||||||
        challenge,
 | 
					 | 
				
			||||||
        allowCredentials,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface AuthAssertion {
 | 
					 | 
				
			||||||
    id: string;
 | 
					 | 
				
			||||||
    rawId: string;
 | 
					 | 
				
			||||||
    type: string;
 | 
					 | 
				
			||||||
    assertionClientExtensions: string;
 | 
					 | 
				
			||||||
    response: {
 | 
					 | 
				
			||||||
        clientDataJSON: string;
 | 
					 | 
				
			||||||
        authenticatorData: string;
 | 
					 | 
				
			||||||
        signature: string;
 | 
					 | 
				
			||||||
        userHandle: string | null;
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * Encodes the binary data in the assertion into strings for posting to the server.
 | 
					 | 
				
			||||||
 * @param {PublicKeyCredential} newAssertion
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
export function transformAssertionForServer(newAssertion: PublicKeyCredential): AuthAssertion {
 | 
					 | 
				
			||||||
    const response = newAssertion.response as AuthenticatorAssertionResponse;
 | 
					 | 
				
			||||||
    const authData = new Uint8Array(response.authenticatorData);
 | 
					 | 
				
			||||||
    const clientDataJSON = new Uint8Array(response.clientDataJSON);
 | 
					 | 
				
			||||||
    const rawId = new Uint8Array(newAssertion.rawId);
 | 
					 | 
				
			||||||
    const sig = new Uint8Array(response.signature);
 | 
					 | 
				
			||||||
    const assertionClientExtensions = newAssertion.getClientExtensionResults();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
        id: newAssertion.id,
 | 
					 | 
				
			||||||
        rawId: b64enc(rawId),
 | 
					 | 
				
			||||||
        type: newAssertion.type,
 | 
					 | 
				
			||||||
        assertionClientExtensions: JSON.stringify(assertionClientExtensions),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        response: {
 | 
					 | 
				
			||||||
            clientDataJSON: b64RawEnc(clientDataJSON),
 | 
					 | 
				
			||||||
            signature: b64RawEnc(sig),
 | 
					 | 
				
			||||||
            authenticatorData: b64RawEnc(authData),
 | 
					 | 
				
			||||||
            userHandle: null,
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,10 +1,10 @@
 | 
				
			|||||||
import {
 | 
					 | 
				
			||||||
    checkWebAuthnSupport,
 | 
					 | 
				
			||||||
    transformAssertionForServer,
 | 
					 | 
				
			||||||
    transformCredentialRequestOptions,
 | 
					 | 
				
			||||||
} from "@goauthentik/common/helpers/webauthn";
 | 
					 | 
				
			||||||
import "@goauthentik/elements/EmptyState";
 | 
					import "@goauthentik/elements/EmptyState";
 | 
				
			||||||
import { BaseDeviceStage } from "@goauthentik/flow/stages/authenticator_validate/base";
 | 
					import { BaseDeviceStage } from "@goauthentik/flow/stages/authenticator_validate/base";
 | 
				
			||||||
 | 
					import { assertWebAuthnSupport } from "@goauthentik/web/authentication";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					    transformAssertionForServer,
 | 
				
			||||||
 | 
					    transformCredentialRequestOptions,
 | 
				
			||||||
 | 
					} from "@goauthentik/web/authentication";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { msg } from "@lit/localize";
 | 
					import { msg } from "@lit/localize";
 | 
				
			||||||
import { PropertyValues, TemplateResult, html, nothing } from "lit";
 | 
					import { PropertyValues, TemplateResult, html, nothing } from "lit";
 | 
				
			||||||
@ -39,7 +39,7 @@ export class AuthenticatorValidateStageWebAuthn extends BaseDeviceStage<
 | 
				
			|||||||
        // request the authenticator to create an assertion signature using the
 | 
					        // request the authenticator to create an assertion signature using the
 | 
				
			||||||
        // credential private key
 | 
					        // credential private key
 | 
				
			||||||
        let assertion;
 | 
					        let assertion;
 | 
				
			||||||
        checkWebAuthnSupport();
 | 
					        assertWebAuthnSupport();
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            assertion = await navigator.credentials.get({
 | 
					            assertion = await navigator.credentials.get({
 | 
				
			||||||
                publicKey: this.transformedCredentialRequestOptions,
 | 
					                publicKey: this.transformedCredentialRequestOptions,
 | 
				
			||||||
 | 
				
			|||||||
@ -1,11 +1,11 @@
 | 
				
			|||||||
import {
 | 
					 | 
				
			||||||
    Assertion,
 | 
					 | 
				
			||||||
    checkWebAuthnSupport,
 | 
					 | 
				
			||||||
    transformCredentialCreateOptions,
 | 
					 | 
				
			||||||
    transformNewAssertionForServer,
 | 
					 | 
				
			||||||
} from "@goauthentik/common/helpers/webauthn";
 | 
					 | 
				
			||||||
import "@goauthentik/elements/EmptyState";
 | 
					import "@goauthentik/elements/EmptyState";
 | 
				
			||||||
import { BaseStage } from "@goauthentik/flow/stages/base";
 | 
					import { BaseStage } from "@goauthentik/flow/stages/base";
 | 
				
			||||||
 | 
					import { assertWebAuthnSupport } from "@goauthentik/web/authentication";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					    Assertion,
 | 
				
			||||||
 | 
					    transformCredentialCreateOptions,
 | 
				
			||||||
 | 
					    transformNewAssertionForServer,
 | 
				
			||||||
 | 
					} from "@goauthentik/web/authentication";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { msg, str } from "@lit/localize";
 | 
					import { msg, str } from "@lit/localize";
 | 
				
			||||||
import { CSSResult, PropertyValues, TemplateResult, css, html, nothing } from "lit";
 | 
					import { CSSResult, PropertyValues, TemplateResult, css, html, nothing } from "lit";
 | 
				
			||||||
@ -66,7 +66,7 @@ export class WebAuthnAuthenticatorRegisterStage extends BaseStage<
 | 
				
			|||||||
        if (!this.challenge) {
 | 
					        if (!this.challenge) {
 | 
				
			||||||
            return;
 | 
					            return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        checkWebAuthnSupport();
 | 
					        assertWebAuthnSupport();
 | 
				
			||||||
        // request the authenticator(s) to create a new credential keypair.
 | 
					        // request the authenticator(s) to create a new credential keypair.
 | 
				
			||||||
        let credential;
 | 
					        let credential;
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
 | 
				
			|||||||
@ -2,6 +2,8 @@
 | 
				
			|||||||
    "compilerOptions": {
 | 
					    "compilerOptions": {
 | 
				
			||||||
        "strict": true,
 | 
					        "strict": true,
 | 
				
			||||||
        "baseUrl": ".",
 | 
					        "baseUrl": ".",
 | 
				
			||||||
 | 
					        "emitDeclarationOnly": true,
 | 
				
			||||||
 | 
					        "declaration": true,
 | 
				
			||||||
        "esModuleInterop": true,
 | 
					        "esModuleInterop": true,
 | 
				
			||||||
        "paths": {
 | 
					        "paths": {
 | 
				
			||||||
            "@goauthentik/docs/*": ["../website/docs/*"]
 | 
					            "@goauthentik/docs/*": ["../website/docs/*"]
 | 
				
			||||||
@ -14,6 +16,7 @@
 | 
				
			|||||||
            "grecaptcha"
 | 
					            "grecaptcha"
 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
        "jsx": "react-jsx",
 | 
					        "jsx": "react-jsx",
 | 
				
			||||||
 | 
					        "allowJs": true,
 | 
				
			||||||
        "skipLibCheck": true,
 | 
					        "skipLibCheck": true,
 | 
				
			||||||
        "forceConsistentCasingInFileNames": true,
 | 
					        "forceConsistentCasingInFileNames": true,
 | 
				
			||||||
        "experimentalDecorators": true,
 | 
					        "experimentalDecorators": true,
 | 
				
			||||||
@ -67,5 +70,12 @@
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "exclude": ["src/**/*.test.ts", "./tests"]
 | 
					    "exclude": [
 | 
				
			||||||
 | 
					        // ---
 | 
				
			||||||
 | 
					        "./out/**/*",
 | 
				
			||||||
 | 
					        "./dist/**/*",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        "src/**/*.test.ts",
 | 
				
			||||||
 | 
					        "./tests"
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,8 +1,15 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
    "extends": "./tsconfig.base.json",
 | 
					    "extends": "./tsconfig.base.json",
 | 
				
			||||||
    "compilerOptions": {
 | 
					    "compilerOptions": {
 | 
				
			||||||
        "types": ["@wdio/types", "grecaptcha", "node", "@wdio/mocha-framework", "expect-webdriverio"],
 | 
					        "types": [
 | 
				
			||||||
 | 
					            "@wdio/types",
 | 
				
			||||||
 | 
					            "grecaptcha",
 | 
				
			||||||
 | 
					            "node",
 | 
				
			||||||
 | 
					            "@wdio/mocha-framework",
 | 
				
			||||||
 | 
					            "expect-webdriverio"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
        "paths": {
 | 
					        "paths": {
 | 
				
			||||||
 | 
					            "@goauthentik/web/authentication": ["./authentication/index.js"],
 | 
				
			||||||
            "@goauthentik/admin/*": ["./src/admin/*"],
 | 
					            "@goauthentik/admin/*": ["./src/admin/*"],
 | 
				
			||||||
            "@goauthentik/common/*": ["./src/common/*"],
 | 
					            "@goauthentik/common/*": ["./src/common/*"],
 | 
				
			||||||
            "@goauthentik/components/*": ["./src/components/*"],
 | 
					            "@goauthentik/components/*": ["./src/components/*"],
 | 
				
			||||||
@ -14,5 +21,5 @@
 | 
				
			|||||||
            "@goauthentik/standalone/*": ["./src/standalone/*"],
 | 
					            "@goauthentik/standalone/*": ["./src/standalone/*"],
 | 
				
			||||||
            "@goauthentik/user/*": ["./src/user/*"]
 | 
					            "@goauthentik/user/*": ["./src/user/*"]
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    },
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -31,5 +31,10 @@
 | 
				
			|||||||
            "@goauthentik/standalone/*": ["./src/standalone/*"],
 | 
					            "@goauthentik/standalone/*": ["./src/standalone/*"],
 | 
				
			||||||
            "@goauthentik/user/*": ["./src/user/*"]
 | 
					            "@goauthentik/user/*": ["./src/user/*"]
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    },
 | 
				
			||||||
 | 
					    "exclude": [
 | 
				
			||||||
 | 
					        // ---
 | 
				
			||||||
 | 
					        "./out/**/*",
 | 
				
			||||||
 | 
					        "./dist/**/*"
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user