web: re-format with prettier
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
		@ -1,5 +1,13 @@
 | 
			
		||||
import { t } from "@lingui/macro";
 | 
			
		||||
import { LitElement, html, customElement, property, TemplateResult, CSSResult, css } from "lit-element";
 | 
			
		||||
import {
 | 
			
		||||
    LitElement,
 | 
			
		||||
    html,
 | 
			
		||||
    customElement,
 | 
			
		||||
    property,
 | 
			
		||||
    TemplateResult,
 | 
			
		||||
    CSSResult,
 | 
			
		||||
    css,
 | 
			
		||||
} from "lit-element";
 | 
			
		||||
 | 
			
		||||
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
 | 
			
		||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
 | 
			
		||||
@ -26,7 +34,15 @@ import "./stages/password/PasswordStage";
 | 
			
		||||
import "./stages/prompt/PromptStage";
 | 
			
		||||
import "./sources/plex/PlexLoginInit";
 | 
			
		||||
import { StageHost } from "./stages/base";
 | 
			
		||||
import { ChallengeChoices, CurrentTenant, ChallengeTypes, FlowChallengeResponseRequest, FlowsApi, RedirectChallenge, ShellChallenge } from "authentik-api";
 | 
			
		||||
import {
 | 
			
		||||
    ChallengeChoices,
 | 
			
		||||
    CurrentTenant,
 | 
			
		||||
    ChallengeTypes,
 | 
			
		||||
    FlowChallengeResponseRequest,
 | 
			
		||||
    FlowsApi,
 | 
			
		||||
    RedirectChallenge,
 | 
			
		||||
    ShellChallenge,
 | 
			
		||||
} from "authentik-api";
 | 
			
		||||
import { DEFAULT_CONFIG, tenant } from "../api/Config";
 | 
			
		||||
import { ifDefined } from "lit-html/directives/if-defined";
 | 
			
		||||
import { until } from "lit-html/directives/until";
 | 
			
		||||
@ -37,13 +53,12 @@ import { WebsocketClient } from "../common/ws";
 | 
			
		||||
 | 
			
		||||
@customElement("ak-flow-executor")
 | 
			
		||||
export class FlowExecutor extends LitElement implements StageHost {
 | 
			
		||||
 | 
			
		||||
    flowSlug: string;
 | 
			
		||||
 | 
			
		||||
    @property({attribute: false})
 | 
			
		||||
    @property({ attribute: false })
 | 
			
		||||
    challenge?: ChallengeTypes;
 | 
			
		||||
 | 
			
		||||
    @property({type: Boolean})
 | 
			
		||||
    @property({ type: Boolean })
 | 
			
		||||
    loading = false;
 | 
			
		||||
 | 
			
		||||
    @property({ attribute: false })
 | 
			
		||||
@ -84,13 +99,15 @@ export class FlowExecutor extends LitElement implements StageHost {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setBackground(url: string): void {
 | 
			
		||||
        this.shadowRoot?.querySelectorAll<HTMLDivElement>(".pf-c-background-image").forEach((bg) => {
 | 
			
		||||
            bg.style.setProperty("--ak-flow-background", `url('${url}')`);
 | 
			
		||||
        });
 | 
			
		||||
        this.shadowRoot
 | 
			
		||||
            ?.querySelectorAll<HTMLDivElement>(".pf-c-background-image")
 | 
			
		||||
            .forEach((bg) => {
 | 
			
		||||
                bg.style.setProperty("--ak-flow-background", `url('${url}')`);
 | 
			
		||||
            });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private postUpdate(): void {
 | 
			
		||||
        tenant().then(tenant => {
 | 
			
		||||
        tenant().then((tenant) => {
 | 
			
		||||
            if (this.challenge?.flowInfo?.title) {
 | 
			
		||||
                document.title = `${this.challenge.flowInfo?.title} - ${tenant.brandingTitle}`;
 | 
			
		||||
            } else {
 | 
			
		||||
@ -105,40 +122,48 @@ export class FlowExecutor extends LitElement implements StageHost {
 | 
			
		||||
        // @ts-ignore
 | 
			
		||||
        payload.component = this.challenge.component;
 | 
			
		||||
        this.loading = true;
 | 
			
		||||
        return new FlowsApi(DEFAULT_CONFIG).flowsExecutorSolve({
 | 
			
		||||
            flowSlug: this.flowSlug,
 | 
			
		||||
            query: window.location.search.substring(1),
 | 
			
		||||
            flowChallengeResponseRequest: payload,
 | 
			
		||||
        }).then((data) => {
 | 
			
		||||
            this.challenge = data;
 | 
			
		||||
            this.postUpdate();
 | 
			
		||||
        }).catch((e: Error | Response) => {
 | 
			
		||||
            this.errorMessage(e);
 | 
			
		||||
        }).finally(() => {
 | 
			
		||||
            this.loading = false;
 | 
			
		||||
        });
 | 
			
		||||
        return new FlowsApi(DEFAULT_CONFIG)
 | 
			
		||||
            .flowsExecutorSolve({
 | 
			
		||||
                flowSlug: this.flowSlug,
 | 
			
		||||
                query: window.location.search.substring(1),
 | 
			
		||||
                flowChallengeResponseRequest: payload,
 | 
			
		||||
            })
 | 
			
		||||
            .then((data) => {
 | 
			
		||||
                this.challenge = data;
 | 
			
		||||
                this.postUpdate();
 | 
			
		||||
            })
 | 
			
		||||
            .catch((e: Error | Response) => {
 | 
			
		||||
                this.errorMessage(e);
 | 
			
		||||
            })
 | 
			
		||||
            .finally(() => {
 | 
			
		||||
                this.loading = false;
 | 
			
		||||
            });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    firstUpdated(): void {
 | 
			
		||||
        configureSentry();
 | 
			
		||||
        tenant().then(tenant => this.tenant = tenant);
 | 
			
		||||
        tenant().then((tenant) => (this.tenant = tenant));
 | 
			
		||||
        this.loading = true;
 | 
			
		||||
        new FlowsApi(DEFAULT_CONFIG).flowsExecutorGet({
 | 
			
		||||
            flowSlug: this.flowSlug,
 | 
			
		||||
            query: window.location.search.substring(1),
 | 
			
		||||
        }).then((challenge) => {
 | 
			
		||||
            this.challenge = challenge;
 | 
			
		||||
            // Only set background on first update, flow won't change throughout execution
 | 
			
		||||
            if (this.challenge?.flowInfo?.background) {
 | 
			
		||||
                this.setBackground(this.challenge.flowInfo.background);
 | 
			
		||||
            }
 | 
			
		||||
            this.postUpdate();
 | 
			
		||||
        }).catch((e: Error | Response) => {
 | 
			
		||||
            // Catch JSON or Update errors
 | 
			
		||||
            this.errorMessage(e);
 | 
			
		||||
        }).finally(() => {
 | 
			
		||||
            this.loading = false;
 | 
			
		||||
        });
 | 
			
		||||
        new FlowsApi(DEFAULT_CONFIG)
 | 
			
		||||
            .flowsExecutorGet({
 | 
			
		||||
                flowSlug: this.flowSlug,
 | 
			
		||||
                query: window.location.search.substring(1),
 | 
			
		||||
            })
 | 
			
		||||
            .then((challenge) => {
 | 
			
		||||
                this.challenge = challenge;
 | 
			
		||||
                // Only set background on first update, flow won't change throughout execution
 | 
			
		||||
                if (this.challenge?.flowInfo?.background) {
 | 
			
		||||
                    this.setBackground(this.challenge.flowInfo.background);
 | 
			
		||||
                }
 | 
			
		||||
                this.postUpdate();
 | 
			
		||||
            })
 | 
			
		||||
            .catch((e: Error | Response) => {
 | 
			
		||||
                // Catch JSON or Update errors
 | 
			
		||||
                this.errorMessage(e);
 | 
			
		||||
            })
 | 
			
		||||
            .finally(() => {
 | 
			
		||||
                this.loading = false;
 | 
			
		||||
            });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async errorMessage(error: Error | Response): Promise<void> {
 | 
			
		||||
@ -167,7 +192,7 @@ export class FlowExecutor extends LitElement implements StageHost {
 | 
			
		||||
                        </a>
 | 
			
		||||
                    </li>
 | 
			
		||||
                </ul>
 | 
			
		||||
            </footer>`
 | 
			
		||||
            </footer>`,
 | 
			
		||||
        } as ChallengeTypes;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -183,46 +208,92 @@ export class FlowExecutor extends LitElement implements StageHost {
 | 
			
		||||
        }
 | 
			
		||||
        switch (this.challenge.type) {
 | 
			
		||||
            case ChallengeChoices.Redirect:
 | 
			
		||||
                console.debug("authentik/flows: redirecting to url from server", (this.challenge as RedirectChallenge).to);
 | 
			
		||||
                console.debug(
 | 
			
		||||
                    "authentik/flows: redirecting to url from server",
 | 
			
		||||
                    (this.challenge as RedirectChallenge).to,
 | 
			
		||||
                );
 | 
			
		||||
                window.location.assign((this.challenge as RedirectChallenge).to);
 | 
			
		||||
                return html`<ak-empty-state
 | 
			
		||||
                        ?loading=${true}
 | 
			
		||||
                        header=${t`Loading`}>
 | 
			
		||||
                    </ak-empty-state>`;
 | 
			
		||||
                return html`<ak-empty-state ?loading=${true} header=${t`Loading`}>
 | 
			
		||||
                </ak-empty-state>`;
 | 
			
		||||
            case ChallengeChoices.Shell:
 | 
			
		||||
                return html`${unsafeHTML((this.challenge as ShellChallenge).body)}`;
 | 
			
		||||
            case ChallengeChoices.Native:
 | 
			
		||||
                switch (this.challenge.component) {
 | 
			
		||||
                    case "ak-stage-access-denied":
 | 
			
		||||
                        return html`<ak-stage-access-denied .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-access-denied>`;
 | 
			
		||||
                        return html`<ak-stage-access-denied
 | 
			
		||||
                            .host=${this as StageHost}
 | 
			
		||||
                            .challenge=${this.challenge}
 | 
			
		||||
                        ></ak-stage-access-denied>`;
 | 
			
		||||
                    case "ak-stage-identification":
 | 
			
		||||
                        return html`<ak-stage-identification .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-identification>`;
 | 
			
		||||
                        return html`<ak-stage-identification
 | 
			
		||||
                            .host=${this as StageHost}
 | 
			
		||||
                            .challenge=${this.challenge}
 | 
			
		||||
                        ></ak-stage-identification>`;
 | 
			
		||||
                    case "ak-stage-password":
 | 
			
		||||
                        return html`<ak-stage-password .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-password>`;
 | 
			
		||||
                        return html`<ak-stage-password
 | 
			
		||||
                            .host=${this as StageHost}
 | 
			
		||||
                            .challenge=${this.challenge}
 | 
			
		||||
                        ></ak-stage-password>`;
 | 
			
		||||
                    case "ak-stage-captcha":
 | 
			
		||||
                        return html`<ak-stage-captcha .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-captcha>`;
 | 
			
		||||
                        return html`<ak-stage-captcha
 | 
			
		||||
                            .host=${this as StageHost}
 | 
			
		||||
                            .challenge=${this.challenge}
 | 
			
		||||
                        ></ak-stage-captcha>`;
 | 
			
		||||
                    case "ak-stage-consent":
 | 
			
		||||
                        return html`<ak-stage-consent .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-consent>`;
 | 
			
		||||
                        return html`<ak-stage-consent
 | 
			
		||||
                            .host=${this as StageHost}
 | 
			
		||||
                            .challenge=${this.challenge}
 | 
			
		||||
                        ></ak-stage-consent>`;
 | 
			
		||||
                    case "ak-stage-dummy":
 | 
			
		||||
                        return html`<ak-stage-dummy .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-dummy>`;
 | 
			
		||||
                        return html`<ak-stage-dummy
 | 
			
		||||
                            .host=${this as StageHost}
 | 
			
		||||
                            .challenge=${this.challenge}
 | 
			
		||||
                        ></ak-stage-dummy>`;
 | 
			
		||||
                    case "ak-stage-email":
 | 
			
		||||
                        return html`<ak-stage-email .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-email>`;
 | 
			
		||||
                        return html`<ak-stage-email
 | 
			
		||||
                            .host=${this as StageHost}
 | 
			
		||||
                            .challenge=${this.challenge}
 | 
			
		||||
                        ></ak-stage-email>`;
 | 
			
		||||
                    case "ak-stage-autosubmit":
 | 
			
		||||
                        return html`<ak-stage-autosubmit .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-autosubmit>`;
 | 
			
		||||
                        return html`<ak-stage-autosubmit
 | 
			
		||||
                            .host=${this as StageHost}
 | 
			
		||||
                            .challenge=${this.challenge}
 | 
			
		||||
                        ></ak-stage-autosubmit>`;
 | 
			
		||||
                    case "ak-stage-prompt":
 | 
			
		||||
                        return html`<ak-stage-prompt .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-prompt>`;
 | 
			
		||||
                        return html`<ak-stage-prompt
 | 
			
		||||
                            .host=${this as StageHost}
 | 
			
		||||
                            .challenge=${this.challenge}
 | 
			
		||||
                        ></ak-stage-prompt>`;
 | 
			
		||||
                    case "ak-stage-authenticator-totp":
 | 
			
		||||
                        return html`<ak-stage-authenticator-totp .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-authenticator-totp>`;
 | 
			
		||||
                        return html`<ak-stage-authenticator-totp
 | 
			
		||||
                            .host=${this as StageHost}
 | 
			
		||||
                            .challenge=${this.challenge}
 | 
			
		||||
                        ></ak-stage-authenticator-totp>`;
 | 
			
		||||
                    case "ak-stage-authenticator-duo":
 | 
			
		||||
                        return html`<ak-stage-authenticator-duo .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-authenticator-duo>`;
 | 
			
		||||
                        return html`<ak-stage-authenticator-duo
 | 
			
		||||
                            .host=${this as StageHost}
 | 
			
		||||
                            .challenge=${this.challenge}
 | 
			
		||||
                        ></ak-stage-authenticator-duo>`;
 | 
			
		||||
                    case "ak-stage-authenticator-static":
 | 
			
		||||
                        return html`<ak-stage-authenticator-static .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-authenticator-static>`;
 | 
			
		||||
                        return html`<ak-stage-authenticator-static
 | 
			
		||||
                            .host=${this as StageHost}
 | 
			
		||||
                            .challenge=${this.challenge}
 | 
			
		||||
                        ></ak-stage-authenticator-static>`;
 | 
			
		||||
                    case "ak-stage-authenticator-webauthn":
 | 
			
		||||
                        return html`<ak-stage-authenticator-webauthn .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-authenticator-webauthn>`;
 | 
			
		||||
                        return html`<ak-stage-authenticator-webauthn
 | 
			
		||||
                            .host=${this as StageHost}
 | 
			
		||||
                            .challenge=${this.challenge}
 | 
			
		||||
                        ></ak-stage-authenticator-webauthn>`;
 | 
			
		||||
                    case "ak-stage-authenticator-validate":
 | 
			
		||||
                        return html`<ak-stage-authenticator-validate .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-authenticator-validate>`;
 | 
			
		||||
                        return html`<ak-stage-authenticator-validate
 | 
			
		||||
                            .host=${this as StageHost}
 | 
			
		||||
                            .challenge=${this.challenge}
 | 
			
		||||
                        ></ak-stage-authenticator-validate>`;
 | 
			
		||||
                    case "ak-flow-sources-plex":
 | 
			
		||||
                        return html`<ak-flow-sources-plex .host=${this as StageHost} .challenge=${this.challenge}></ak-flow-sources-plex>`;
 | 
			
		||||
                        return html`<ak-flow-sources-plex
 | 
			
		||||
                            .host=${this as StageHost}
 | 
			
		||||
                            .challenge=${this.challenge}
 | 
			
		||||
                        ></ak-flow-sources-plex>`;
 | 
			
		||||
                    default:
 | 
			
		||||
                        break;
 | 
			
		||||
                }
 | 
			
		||||
@ -236,59 +307,85 @@ export class FlowExecutor extends LitElement implements StageHost {
 | 
			
		||||
 | 
			
		||||
    renderChallengeWrapper(): TemplateResult {
 | 
			
		||||
        if (!this.challenge) {
 | 
			
		||||
            return html`<ak-empty-state
 | 
			
		||||
                    ?loading=${true}
 | 
			
		||||
                    header=${t`Loading`}>
 | 
			
		||||
            </ak-empty-state>`;
 | 
			
		||||
            return html`<ak-empty-state ?loading=${true} header=${t`Loading`}> </ak-empty-state>`;
 | 
			
		||||
        }
 | 
			
		||||
        return html`
 | 
			
		||||
            ${this.loading ? this.renderLoading() : html``}
 | 
			
		||||
            ${this.renderChallenge()}
 | 
			
		||||
        `;
 | 
			
		||||
        return html` ${this.loading ? this.renderLoading() : html``} ${this.renderChallenge()} `;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render(): TemplateResult {
 | 
			
		||||
        return html`<div class="pf-c-background-image">
 | 
			
		||||
            <svg xmlns="http://www.w3.org/2000/svg" class="pf-c-background-image__filter" width="0" height="0">
 | 
			
		||||
                <filter id="image_overlay">
 | 
			
		||||
                    <feColorMatrix in="SourceGraphic" type="matrix" values="1.3 0 0 0 0 0 1.3 0 0 0 0 0 1.3 0 0 0 0 0 1 0" />
 | 
			
		||||
                    <feComponentTransfer color-interpolation-filters="sRGB" result="duotone">
 | 
			
		||||
                        <feFuncR type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncR>
 | 
			
		||||
                        <feFuncG type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncG>
 | 
			
		||||
                        <feFuncB type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncB>
 | 
			
		||||
                        <feFuncA type="table" tableValues="0 1"></feFuncA>
 | 
			
		||||
                    </feComponentTransfer>
 | 
			
		||||
                </filter>
 | 
			
		||||
            </svg>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="pf-c-login">
 | 
			
		||||
            <div class="ak-login-container">
 | 
			
		||||
                <header class="pf-c-login__header">
 | 
			
		||||
                    <div class="pf-c-brand ak-brand">
 | 
			
		||||
                        <img src="${ifDefined(this.tenant?.brandingLogo)}" alt="authentik icon" />
 | 
			
		||||
                    </div>
 | 
			
		||||
                </header>
 | 
			
		||||
                <div class="pf-c-login__main">
 | 
			
		||||
                    ${this.renderChallengeWrapper()}
 | 
			
		||||
                </div>
 | 
			
		||||
                <footer class="pf-c-login__footer">
 | 
			
		||||
                    <p></p>
 | 
			
		||||
                    <ul class="pf-c-list pf-m-inline">
 | 
			
		||||
                        ${until(this.tenant?.uiFooterLinks?.map((link) => {
 | 
			
		||||
                            return html`<li>
 | 
			
		||||
                                <a href="${link.href || ""}">${link.name}</a>
 | 
			
		||||
                            </li>`;
 | 
			
		||||
                        }))}
 | 
			
		||||
                        ${this.tenant?.brandingTitle != "authentik" ? html`
 | 
			
		||||
                            <li><a href="https://goauthentik.io">${t`Powered by authentik`}</a></li>
 | 
			
		||||
                        ` : html``}
 | 
			
		||||
                        ${this.challenge?.flowInfo?.background?.startsWith("/static") ? html`
 | 
			
		||||
                            <li><a href="https://unsplash.com/@ventiviews">${t`Background image`}</a></li>
 | 
			
		||||
                        ` : html``}
 | 
			
		||||
                    </ul>
 | 
			
		||||
                </footer>
 | 
			
		||||
                <svg
 | 
			
		||||
                    xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
                    class="pf-c-background-image__filter"
 | 
			
		||||
                    width="0"
 | 
			
		||||
                    height="0"
 | 
			
		||||
                >
 | 
			
		||||
                    <filter id="image_overlay">
 | 
			
		||||
                        <feColorMatrix
 | 
			
		||||
                            in="SourceGraphic"
 | 
			
		||||
                            type="matrix"
 | 
			
		||||
                            values="1.3 0 0 0 0 0 1.3 0 0 0 0 0 1.3 0 0 0 0 0 1 0"
 | 
			
		||||
                        />
 | 
			
		||||
                        <feComponentTransfer color-interpolation-filters="sRGB" result="duotone">
 | 
			
		||||
                            <feFuncR
 | 
			
		||||
                                type="table"
 | 
			
		||||
                                tableValues="0.086274509803922 0.43921568627451"
 | 
			
		||||
                            ></feFuncR>
 | 
			
		||||
                            <feFuncG
 | 
			
		||||
                                type="table"
 | 
			
		||||
                                tableValues="0.086274509803922 0.43921568627451"
 | 
			
		||||
                            ></feFuncG>
 | 
			
		||||
                            <feFuncB
 | 
			
		||||
                                type="table"
 | 
			
		||||
                                tableValues="0.086274509803922 0.43921568627451"
 | 
			
		||||
                            ></feFuncB>
 | 
			
		||||
                            <feFuncA type="table" tableValues="0 1"></feFuncA>
 | 
			
		||||
                        </feComponentTransfer>
 | 
			
		||||
                    </filter>
 | 
			
		||||
                </svg>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>`;
 | 
			
		||||
            <div class="pf-c-login">
 | 
			
		||||
                <div class="ak-login-container">
 | 
			
		||||
                    <header class="pf-c-login__header">
 | 
			
		||||
                        <div class="pf-c-brand ak-brand">
 | 
			
		||||
                            <img
 | 
			
		||||
                                src="${ifDefined(this.tenant?.brandingLogo)}"
 | 
			
		||||
                                alt="authentik icon"
 | 
			
		||||
                            />
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </header>
 | 
			
		||||
                    <div class="pf-c-login__main">${this.renderChallengeWrapper()}</div>
 | 
			
		||||
                    <footer class="pf-c-login__footer">
 | 
			
		||||
                        <p></p>
 | 
			
		||||
                        <ul class="pf-c-list pf-m-inline">
 | 
			
		||||
                            ${until(
 | 
			
		||||
                                this.tenant?.uiFooterLinks?.map((link) => {
 | 
			
		||||
                                    return html`<li>
 | 
			
		||||
                                        <a href="${link.href || ""}">${link.name}</a>
 | 
			
		||||
                                    </li>`;
 | 
			
		||||
                                }),
 | 
			
		||||
                            )}
 | 
			
		||||
                            ${this.tenant?.brandingTitle != "authentik"
 | 
			
		||||
                                ? html`
 | 
			
		||||
                                      <li>
 | 
			
		||||
                                          <a href="https://goauthentik.io"
 | 
			
		||||
                                              >${t`Powered by authentik`}</a
 | 
			
		||||
                                          >
 | 
			
		||||
                                      </li>
 | 
			
		||||
                                  `
 | 
			
		||||
                                : html``}
 | 
			
		||||
                            ${this.challenge?.flowInfo?.background?.startsWith("/static")
 | 
			
		||||
                                ? html`
 | 
			
		||||
                                      <li>
 | 
			
		||||
                                          <a href="https://unsplash.com/@ventiviews"
 | 
			
		||||
                                              >${t`Background image`}</a
 | 
			
		||||
                                          >
 | 
			
		||||
                                      </li>
 | 
			
		||||
                                  `
 | 
			
		||||
                                : html``}
 | 
			
		||||
                        </ul>
 | 
			
		||||
                    </footer>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,11 +1,18 @@
 | 
			
		||||
import { t } from "@lingui/macro";
 | 
			
		||||
import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
 | 
			
		||||
import {
 | 
			
		||||
    css,
 | 
			
		||||
    CSSResult,
 | 
			
		||||
    customElement,
 | 
			
		||||
    html,
 | 
			
		||||
    LitElement,
 | 
			
		||||
    property,
 | 
			
		||||
    TemplateResult,
 | 
			
		||||
} from "lit-element";
 | 
			
		||||
import PFAvatar from "@patternfly/patternfly/components/Avatar/avatar.css";
 | 
			
		||||
import { ifDefined } from "lit-html/directives/if-defined";
 | 
			
		||||
 | 
			
		||||
@customElement("ak-form-static")
 | 
			
		||||
export class FormStatic extends LitElement {
 | 
			
		||||
 | 
			
		||||
    @property()
 | 
			
		||||
    userAvatar?: string;
 | 
			
		||||
 | 
			
		||||
@ -13,39 +20,45 @@ export class FormStatic extends LitElement {
 | 
			
		||||
    user = "";
 | 
			
		||||
 | 
			
		||||
    static get styles(): CSSResult[] {
 | 
			
		||||
        return [PFAvatar, css`
 | 
			
		||||
            /* Form with user */
 | 
			
		||||
            .form-control-static {
 | 
			
		||||
                margin-top: var(--pf-global--spacer--sm);
 | 
			
		||||
                display: flex;
 | 
			
		||||
                align-items: center;
 | 
			
		||||
                justify-content: space-between;
 | 
			
		||||
            }
 | 
			
		||||
            .form-control-static .avatar {
 | 
			
		||||
                display: flex;
 | 
			
		||||
                align-items: center;
 | 
			
		||||
            }
 | 
			
		||||
            .form-control-static img {
 | 
			
		||||
                margin-right: var(--pf-global--spacer--xs);
 | 
			
		||||
            }
 | 
			
		||||
            .form-control-static a {
 | 
			
		||||
                padding-top: var(--pf-global--spacer--xs);
 | 
			
		||||
                padding-bottom: var(--pf-global--spacer--xs);
 | 
			
		||||
                line-height: var(--pf-global--spacer--xl);
 | 
			
		||||
            }
 | 
			
		||||
        `];
 | 
			
		||||
        return [
 | 
			
		||||
            PFAvatar,
 | 
			
		||||
            css`
 | 
			
		||||
                /* Form with user */
 | 
			
		||||
                .form-control-static {
 | 
			
		||||
                    margin-top: var(--pf-global--spacer--sm);
 | 
			
		||||
                    display: flex;
 | 
			
		||||
                    align-items: center;
 | 
			
		||||
                    justify-content: space-between;
 | 
			
		||||
                }
 | 
			
		||||
                .form-control-static .avatar {
 | 
			
		||||
                    display: flex;
 | 
			
		||||
                    align-items: center;
 | 
			
		||||
                }
 | 
			
		||||
                .form-control-static img {
 | 
			
		||||
                    margin-right: var(--pf-global--spacer--xs);
 | 
			
		||||
                }
 | 
			
		||||
                .form-control-static a {
 | 
			
		||||
                    padding-top: var(--pf-global--spacer--xs);
 | 
			
		||||
                    padding-bottom: var(--pf-global--spacer--xs);
 | 
			
		||||
                    line-height: var(--pf-global--spacer--xl);
 | 
			
		||||
                }
 | 
			
		||||
            `,
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render(): TemplateResult {
 | 
			
		||||
        return html`
 | 
			
		||||
            <div class="form-control-static">
 | 
			
		||||
                <div class="avatar">
 | 
			
		||||
                    <img class="pf-c-avatar" src="${ifDefined(this.userAvatar)}" alt="${t`User's avatar`}">
 | 
			
		||||
                    <img
 | 
			
		||||
                        class="pf-c-avatar"
 | 
			
		||||
                        src="${ifDefined(this.userAvatar)}"
 | 
			
		||||
                        alt="${t`User's avatar`}"
 | 
			
		||||
                    />
 | 
			
		||||
                    ${this.user}
 | 
			
		||||
                </div>
 | 
			
		||||
                <slot name="link"></slot>
 | 
			
		||||
            </div>
 | 
			
		||||
        `;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -13,23 +13,20 @@ import { t } from "@lingui/macro";
 | 
			
		||||
import "../../elements/EmptyState";
 | 
			
		||||
 | 
			
		||||
@customElement("ak-stage-access-denied")
 | 
			
		||||
export class FlowAccessDenied extends BaseStage<AccessDeniedChallenge, FlowChallengeResponseRequest> {
 | 
			
		||||
 | 
			
		||||
export class FlowAccessDenied extends BaseStage<
 | 
			
		||||
    AccessDeniedChallenge,
 | 
			
		||||
    FlowChallengeResponseRequest
 | 
			
		||||
> {
 | 
			
		||||
    static get styles(): CSSResult[] {
 | 
			
		||||
        return [PFBase, PFLogin, PFForm, PFList, PFFormControl, PFTitle, AKGlobal];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render(): TemplateResult {
 | 
			
		||||
        if (!this.challenge) {
 | 
			
		||||
            return html`<ak-empty-state
 | 
			
		||||
                ?loading="${true}"
 | 
			
		||||
                header=${t`Loading`}>
 | 
			
		||||
            </ak-empty-state>`;
 | 
			
		||||
            return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
 | 
			
		||||
        }
 | 
			
		||||
        return html`<header class="pf-c-login__main-header">
 | 
			
		||||
                <h1 class="pf-c-title pf-m-3xl">
 | 
			
		||||
                    ${this.challenge.flowInfo?.title}
 | 
			
		||||
                </h1>
 | 
			
		||||
                <h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
 | 
			
		||||
            </header>
 | 
			
		||||
            <div class="pf-c-login__main-body">
 | 
			
		||||
                <form method="POST" class="pf-c-form">
 | 
			
		||||
@ -39,15 +36,13 @@ export class FlowAccessDenied extends BaseStage<AccessDeniedChallenge, FlowChall
 | 
			
		||||
                            ${t`Request has been denied.`}
 | 
			
		||||
                        </p>
 | 
			
		||||
                        ${this.challenge?.errorMessage &&
 | 
			
		||||
                            html`<hr>
 | 
			
		||||
                        html`<hr />
 | 
			
		||||
                            <p>${this.challenge.errorMessage}</p>`}
 | 
			
		||||
                    </div>
 | 
			
		||||
                </form>
 | 
			
		||||
            </div>
 | 
			
		||||
            <footer class="pf-c-login__main-footer">
 | 
			
		||||
                <ul class="pf-c-login__main-footer-links">
 | 
			
		||||
                </ul>
 | 
			
		||||
                <ul class="pf-c-login__main-footer-links"></ul>
 | 
			
		||||
            </footer>`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -23,40 +23,54 @@ export const DEFAULT_HEADERS = {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function popupCenterScreen(url: string, title: string, w: number, h: number): Window | null {
 | 
			
		||||
    const top = (screen.height - h) / 4, left = (screen.width - w) / 2;
 | 
			
		||||
    const popup = window.open(url, title, `scrollbars=yes,width=${w},height=${h},top=${top},left=${left}`);
 | 
			
		||||
    const top = (screen.height - h) / 4,
 | 
			
		||||
        left = (screen.width - w) / 2;
 | 
			
		||||
    const popup = window.open(
 | 
			
		||||
        url,
 | 
			
		||||
        title,
 | 
			
		||||
        `scrollbars=yes,width=${w},height=${h},top=${top},left=${left}`,
 | 
			
		||||
    );
 | 
			
		||||
    return popup;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class PlexAPIClient {
 | 
			
		||||
 | 
			
		||||
    token: string;
 | 
			
		||||
 | 
			
		||||
    constructor(token: string) {
 | 
			
		||||
        this.token = token;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static async getPin(clientIdentifier: string): Promise<{ authUrl: string, pin: PlexPinResponse }> {
 | 
			
		||||
        const headers = { ...DEFAULT_HEADERS, ...{
 | 
			
		||||
            "X-Plex-Client-Identifier": clientIdentifier
 | 
			
		||||
        }};
 | 
			
		||||
    static async getPin(
 | 
			
		||||
        clientIdentifier: string,
 | 
			
		||||
    ): Promise<{ authUrl: string; pin: PlexPinResponse }> {
 | 
			
		||||
        const headers = {
 | 
			
		||||
            ...DEFAULT_HEADERS,
 | 
			
		||||
            ...{
 | 
			
		||||
                "X-Plex-Client-Identifier": clientIdentifier,
 | 
			
		||||
            },
 | 
			
		||||
        };
 | 
			
		||||
        const pinResponse = await fetch("https://plex.tv/api/v2/pins.json?strong=true", {
 | 
			
		||||
            method: "POST",
 | 
			
		||||
            headers: headers
 | 
			
		||||
            headers: headers,
 | 
			
		||||
        });
 | 
			
		||||
        const pin: PlexPinResponse = await pinResponse.json();
 | 
			
		||||
        return {
 | 
			
		||||
            authUrl: `https://app.plex.tv/auth#!?clientID=${encodeURIComponent(clientIdentifier)}&code=${pin.code}`,
 | 
			
		||||
            pin: pin
 | 
			
		||||
            authUrl: `https://app.plex.tv/auth#!?clientID=${encodeURIComponent(
 | 
			
		||||
                clientIdentifier,
 | 
			
		||||
            )}&code=${pin.code}`,
 | 
			
		||||
            pin: pin,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static async pinStatus(clientIdentifier: string, id: number): Promise<string | undefined> {
 | 
			
		||||
        const headers = { ...DEFAULT_HEADERS, ...{
 | 
			
		||||
            "X-Plex-Client-Identifier": clientIdentifier
 | 
			
		||||
        }};
 | 
			
		||||
        const headers = {
 | 
			
		||||
            ...DEFAULT_HEADERS,
 | 
			
		||||
            ...{
 | 
			
		||||
                "X-Plex-Client-Identifier": clientIdentifier,
 | 
			
		||||
            },
 | 
			
		||||
        };
 | 
			
		||||
        const pinResponse = await fetch(`https://plex.tv/api/v2/pins/${id}`, {
 | 
			
		||||
            headers: headers
 | 
			
		||||
            headers: headers,
 | 
			
		||||
        });
 | 
			
		||||
        const pin: PlexPinResponse = await pinResponse.json();
 | 
			
		||||
        return pin.authToken || "";
 | 
			
		||||
@ -65,7 +79,7 @@ export class PlexAPIClient {
 | 
			
		||||
    static async pinPoll(clientIdentifier: string, id: number): Promise<string> {
 | 
			
		||||
        const executePoll = async (
 | 
			
		||||
            resolve: (authToken: string) => void,
 | 
			
		||||
            reject: (e: Error) => void
 | 
			
		||||
            reject: (e: Error) => void,
 | 
			
		||||
        ) => {
 | 
			
		||||
            try {
 | 
			
		||||
                const response = await PlexAPIClient.pinStatus(clientIdentifier, id);
 | 
			
		||||
@ -84,13 +98,15 @@ export class PlexAPIClient {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getServers(): Promise<PlexResource[]> {
 | 
			
		||||
        const resourcesResponse = await fetch(`https://plex.tv/api/v2/resources?X-Plex-Token=${this.token}&X-Plex-Client-Identifier=authentik`, {
 | 
			
		||||
            headers: DEFAULT_HEADERS
 | 
			
		||||
        });
 | 
			
		||||
        const resourcesResponse = await fetch(
 | 
			
		||||
            `https://plex.tv/api/v2/resources?X-Plex-Token=${this.token}&X-Plex-Client-Identifier=authentik`,
 | 
			
		||||
            {
 | 
			
		||||
                headers: DEFAULT_HEADERS,
 | 
			
		||||
            },
 | 
			
		||||
        );
 | 
			
		||||
        const resources: PlexResource[] = await resourcesResponse.json();
 | 
			
		||||
        return resources.filter(r => {
 | 
			
		||||
        return resources.filter((r) => {
 | 
			
		||||
            return r.provides.toLowerCase().includes("server") && r.owned;
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,8 @@
 | 
			
		||||
import { t } from "@lingui/macro";
 | 
			
		||||
import { PlexAuthenticationChallenge, PlexAuthenticationChallengeResponseRequest } from "authentik-api";
 | 
			
		||||
import {
 | 
			
		||||
    PlexAuthenticationChallenge,
 | 
			
		||||
    PlexAuthenticationChallengeResponseRequest,
 | 
			
		||||
} from "authentik-api";
 | 
			
		||||
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
 | 
			
		||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
 | 
			
		||||
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
 | 
			
		||||
@ -16,10 +19,11 @@ import { SourcesApi } from "authentik-api";
 | 
			
		||||
import { showMessage } from "../../../elements/messages/MessageContainer";
 | 
			
		||||
import { MessageLevel } from "../../../elements/messages/Message";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@customElement("ak-flow-sources-plex")
 | 
			
		||||
export class PlexLoginInit extends BaseStage<PlexAuthenticationChallenge, PlexAuthenticationChallengeResponseRequest> {
 | 
			
		||||
 | 
			
		||||
export class PlexLoginInit extends BaseStage<
 | 
			
		||||
    PlexAuthenticationChallenge,
 | 
			
		||||
    PlexAuthenticationChallengeResponseRequest
 | 
			
		||||
> {
 | 
			
		||||
    static get styles(): CSSResult[] {
 | 
			
		||||
        return [PFBase, PFLogin, PFForm, PFFormControl, PFButton, PFTitle, AKGlobal];
 | 
			
		||||
    }
 | 
			
		||||
@ -27,46 +31,43 @@ export class PlexLoginInit extends BaseStage<PlexAuthenticationChallenge, PlexAu
 | 
			
		||||
    async firstUpdated(): Promise<void> {
 | 
			
		||||
        const authInfo = await PlexAPIClient.getPin(this.challenge?.clientId || "");
 | 
			
		||||
        const authWindow = popupCenterScreen(authInfo.authUrl, "plex auth", 550, 700);
 | 
			
		||||
        PlexAPIClient.pinPoll(this.challenge?.clientId || "", authInfo.pin.id).then(token => {
 | 
			
		||||
        PlexAPIClient.pinPoll(this.challenge?.clientId || "", authInfo.pin.id).then((token) => {
 | 
			
		||||
            authWindow?.close();
 | 
			
		||||
            new SourcesApi(DEFAULT_CONFIG).sourcesPlexRedeemTokenCreate({
 | 
			
		||||
                plexTokenRedeemRequest: {
 | 
			
		||||
                    plexToken: token,
 | 
			
		||||
                },
 | 
			
		||||
                slug: this.challenge?.slug || "",
 | 
			
		||||
            }).then((r) => {
 | 
			
		||||
                window.location.assign(r.to);
 | 
			
		||||
            }).catch((r: Response) => {
 | 
			
		||||
                r.json().then((body: {detail: string}) => {
 | 
			
		||||
                    showMessage({
 | 
			
		||||
                        level: MessageLevel.error,
 | 
			
		||||
                        message: body.detail
 | 
			
		||||
            new SourcesApi(DEFAULT_CONFIG)
 | 
			
		||||
                .sourcesPlexRedeemTokenCreate({
 | 
			
		||||
                    plexTokenRedeemRequest: {
 | 
			
		||||
                        plexToken: token,
 | 
			
		||||
                    },
 | 
			
		||||
                    slug: this.challenge?.slug || "",
 | 
			
		||||
                })
 | 
			
		||||
                .then((r) => {
 | 
			
		||||
                    window.location.assign(r.to);
 | 
			
		||||
                })
 | 
			
		||||
                .catch((r: Response) => {
 | 
			
		||||
                    r.json().then((body: { detail: string }) => {
 | 
			
		||||
                        showMessage({
 | 
			
		||||
                            level: MessageLevel.error,
 | 
			
		||||
                            message: body.detail,
 | 
			
		||||
                        });
 | 
			
		||||
                        setTimeout(() => {
 | 
			
		||||
                            window.location.assign("/");
 | 
			
		||||
                        }, 5000);
 | 
			
		||||
                    });
 | 
			
		||||
                    setTimeout(() => {
 | 
			
		||||
                        window.location.assign("/");
 | 
			
		||||
                    }, 5000);
 | 
			
		||||
                });
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render(): TemplateResult {
 | 
			
		||||
        return html`<header class="pf-c-login__main-header">
 | 
			
		||||
                <h1 class="pf-c-title pf-m-3xl">
 | 
			
		||||
                    ${t`Authenticating with Plex...`}
 | 
			
		||||
                </h1>
 | 
			
		||||
                <h1 class="pf-c-title pf-m-3xl">${t`Authenticating with Plex...`}</h1>
 | 
			
		||||
            </header>
 | 
			
		||||
            <div class="pf-c-login__main-body">
 | 
			
		||||
                <form class="pf-c-form">
 | 
			
		||||
                    <ak-empty-state
 | 
			
		||||
                        ?loading="${true}">
 | 
			
		||||
                    </ak-empty-state>
 | 
			
		||||
                    <ak-empty-state ?loading="${true}"> </ak-empty-state>
 | 
			
		||||
                </form>
 | 
			
		||||
            </div>
 | 
			
		||||
            <footer class="pf-c-login__main-footer">
 | 
			
		||||
                <ul class="pf-c-login__main-footer-links">
 | 
			
		||||
                </ul>
 | 
			
		||||
                <ul class="pf-c-login__main-footer-links"></ul>
 | 
			
		||||
            </footer>`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -11,13 +11,19 @@ import { BaseStage } from "../base";
 | 
			
		||||
import "../../../elements/forms/FormElement";
 | 
			
		||||
import "../../../elements/EmptyState";
 | 
			
		||||
import "../../FormStatic";
 | 
			
		||||
import { AuthenticatorDuoChallenge, AuthenticatorDuoChallengeResponseRequest, StagesApi } from "authentik-api";
 | 
			
		||||
import {
 | 
			
		||||
    AuthenticatorDuoChallenge,
 | 
			
		||||
    AuthenticatorDuoChallengeResponseRequest,
 | 
			
		||||
    StagesApi,
 | 
			
		||||
} from "authentik-api";
 | 
			
		||||
import { DEFAULT_CONFIG } from "../../../api/Config";
 | 
			
		||||
import { ifDefined } from "lit-html/directives/if-defined";
 | 
			
		||||
 | 
			
		||||
@customElement("ak-stage-authenticator-duo")
 | 
			
		||||
export class AuthenticatorDuoStage extends BaseStage<AuthenticatorDuoChallenge, AuthenticatorDuoChallengeResponseRequest> {
 | 
			
		||||
 | 
			
		||||
export class AuthenticatorDuoStage extends BaseStage<
 | 
			
		||||
    AuthenticatorDuoChallenge,
 | 
			
		||||
    AuthenticatorDuoChallengeResponseRequest
 | 
			
		||||
> {
 | 
			
		||||
    static get styles(): CSSResult[] {
 | 
			
		||||
        return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal];
 | 
			
		||||
    }
 | 
			
		||||
@ -31,35 +37,41 @@ export class AuthenticatorDuoStage extends BaseStage<AuthenticatorDuoChallenge,
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    checkEnrollStatus(): Promise<void> {
 | 
			
		||||
        return new StagesApi(DEFAULT_CONFIG).stagesAuthenticatorDuoEnrollmentStatusCreate({
 | 
			
		||||
            stageUuid: this.challenge?.stageUuid || "",
 | 
			
		||||
        }).then(() => {
 | 
			
		||||
            this.host?.submit({});
 | 
			
		||||
        }).catch(() => {
 | 
			
		||||
            console.debug("authentik/flows/duo: Waiting for auth status");
 | 
			
		||||
        });
 | 
			
		||||
        return new StagesApi(DEFAULT_CONFIG)
 | 
			
		||||
            .stagesAuthenticatorDuoEnrollmentStatusCreate({
 | 
			
		||||
                stageUuid: this.challenge?.stageUuid || "",
 | 
			
		||||
            })
 | 
			
		||||
            .then(() => {
 | 
			
		||||
                this.host?.submit({});
 | 
			
		||||
            })
 | 
			
		||||
            .catch(() => {
 | 
			
		||||
                console.debug("authentik/flows/duo: Waiting for auth status");
 | 
			
		||||
            });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render(): TemplateResult {
 | 
			
		||||
        if (!this.challenge) {
 | 
			
		||||
            return html`<ak-empty-state
 | 
			
		||||
                ?loading="${true}"
 | 
			
		||||
                header=${t`Loading`}>
 | 
			
		||||
            </ak-empty-state>`;
 | 
			
		||||
            return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
 | 
			
		||||
        }
 | 
			
		||||
        return html`<header class="pf-c-login__main-header">
 | 
			
		||||
                <h1 class="pf-c-title pf-m-3xl">
 | 
			
		||||
                    ${this.challenge.flowInfo?.title}
 | 
			
		||||
                </h1>
 | 
			
		||||
                <h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
 | 
			
		||||
            </header>
 | 
			
		||||
            <div class="pf-c-login__main-body">
 | 
			
		||||
                <form class="pf-c-form" @submit=${(e: Event) => { this.submitForm(e); }}>
 | 
			
		||||
                <form
 | 
			
		||||
                    class="pf-c-form"
 | 
			
		||||
                    @submit=${(e: Event) => {
 | 
			
		||||
                        this.submitForm(e);
 | 
			
		||||
                    }}
 | 
			
		||||
                >
 | 
			
		||||
                    <ak-form-static
 | 
			
		||||
                        class="pf-c-form__group"
 | 
			
		||||
                        userAvatar="${this.challenge.pendingUserAvatar}"
 | 
			
		||||
                        user=${this.challenge.pendingUser}>
 | 
			
		||||
                        user=${this.challenge.pendingUser}
 | 
			
		||||
                    >
 | 
			
		||||
                        <div slot="link">
 | 
			
		||||
                            <a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}">${t`Not you?`}</a>
 | 
			
		||||
                            <a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
 | 
			
		||||
                                >${t`Not you?`}</a
 | 
			
		||||
                            >
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </ak-form-static>
 | 
			
		||||
                    <img src=${this.challenge.activationBarcode} />
 | 
			
		||||
@ -69,18 +81,20 @@ export class AuthenticatorDuoStage extends BaseStage<AuthenticatorDuoChallenge,
 | 
			
		||||
                    <a href=${this.challenge.activationCode}>${t`Duo activation`}</a>
 | 
			
		||||
 | 
			
		||||
                    <div class="pf-c-form__group pf-m-action">
 | 
			
		||||
                        <button type="button" class="pf-c-button pf-m-primary pf-m-block" @click=${() => {
 | 
			
		||||
                            this.checkEnrollStatus();
 | 
			
		||||
                        }}>
 | 
			
		||||
                        <button
 | 
			
		||||
                            type="button"
 | 
			
		||||
                            class="pf-c-button pf-m-primary pf-m-block"
 | 
			
		||||
                            @click=${() => {
 | 
			
		||||
                                this.checkEnrollStatus();
 | 
			
		||||
                            }}
 | 
			
		||||
                        >
 | 
			
		||||
                            ${t`Check status`}
 | 
			
		||||
                        </button>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </form>
 | 
			
		||||
            </div>
 | 
			
		||||
            <footer class="pf-c-login__main-footer">
 | 
			
		||||
                <ul class="pf-c-login__main-footer-links">
 | 
			
		||||
                </ul>
 | 
			
		||||
                <ul class="pf-c-login__main-footer-links"></ul>
 | 
			
		||||
            </footer>`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -11,58 +11,75 @@ import { BaseStage } from "../base";
 | 
			
		||||
import "../../../elements/forms/FormElement";
 | 
			
		||||
import "../../../elements/EmptyState";
 | 
			
		||||
import "../../FormStatic";
 | 
			
		||||
import { AuthenticatorStaticChallenge, AuthenticatorStaticChallengeResponseRequest } from "authentik-api";
 | 
			
		||||
import {
 | 
			
		||||
    AuthenticatorStaticChallenge,
 | 
			
		||||
    AuthenticatorStaticChallengeResponseRequest,
 | 
			
		||||
} from "authentik-api";
 | 
			
		||||
import { ifDefined } from "lit-html/directives/if-defined";
 | 
			
		||||
 | 
			
		||||
export const STATIC_TOKEN_STYLE = css`
 | 
			
		||||
/* Static OTP Tokens */
 | 
			
		||||
.ak-otp-tokens {
 | 
			
		||||
    list-style: circle;
 | 
			
		||||
    columns: 2;
 | 
			
		||||
    -webkit-columns: 2;
 | 
			
		||||
    -moz-columns: 2;
 | 
			
		||||
    margin-left: var(--pf-global--spacer--xs);
 | 
			
		||||
}
 | 
			
		||||
.ak-otp-tokens li {
 | 
			
		||||
    font-size: var(--pf-global--FontSize--2xl);
 | 
			
		||||
    font-family: monospace;
 | 
			
		||||
}
 | 
			
		||||
    /* Static OTP Tokens */
 | 
			
		||||
    .ak-otp-tokens {
 | 
			
		||||
        list-style: circle;
 | 
			
		||||
        columns: 2;
 | 
			
		||||
        -webkit-columns: 2;
 | 
			
		||||
        -moz-columns: 2;
 | 
			
		||||
        margin-left: var(--pf-global--spacer--xs);
 | 
			
		||||
    }
 | 
			
		||||
    .ak-otp-tokens li {
 | 
			
		||||
        font-size: var(--pf-global--FontSize--2xl);
 | 
			
		||||
        font-family: monospace;
 | 
			
		||||
    }
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@customElement("ak-stage-authenticator-static")
 | 
			
		||||
export class AuthenticatorStaticStage extends BaseStage<AuthenticatorStaticChallenge, AuthenticatorStaticChallengeResponseRequest> {
 | 
			
		||||
 | 
			
		||||
export class AuthenticatorStaticStage extends BaseStage<
 | 
			
		||||
    AuthenticatorStaticChallenge,
 | 
			
		||||
    AuthenticatorStaticChallengeResponseRequest
 | 
			
		||||
> {
 | 
			
		||||
    static get styles(): CSSResult[] {
 | 
			
		||||
        return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal, STATIC_TOKEN_STYLE];
 | 
			
		||||
        return [
 | 
			
		||||
            PFBase,
 | 
			
		||||
            PFLogin,
 | 
			
		||||
            PFForm,
 | 
			
		||||
            PFFormControl,
 | 
			
		||||
            PFTitle,
 | 
			
		||||
            PFButton,
 | 
			
		||||
            AKGlobal,
 | 
			
		||||
            STATIC_TOKEN_STYLE,
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render(): TemplateResult {
 | 
			
		||||
        if (!this.challenge) {
 | 
			
		||||
            return html`<ak-empty-state
 | 
			
		||||
                ?loading="${true}"
 | 
			
		||||
                header=${t`Loading`}>
 | 
			
		||||
            </ak-empty-state>`;
 | 
			
		||||
            return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
 | 
			
		||||
        }
 | 
			
		||||
        return html`<header class="pf-c-login__main-header">
 | 
			
		||||
                <h1 class="pf-c-title pf-m-3xl">
 | 
			
		||||
                    ${this.challenge.flowInfo?.title}
 | 
			
		||||
                </h1>
 | 
			
		||||
                <h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
 | 
			
		||||
            </header>
 | 
			
		||||
            <div class="pf-c-login__main-body">
 | 
			
		||||
                <form class="pf-c-form" @submit=${(e: Event) => { this.submitForm(e); }}>
 | 
			
		||||
                <form
 | 
			
		||||
                    class="pf-c-form"
 | 
			
		||||
                    @submit=${(e: Event) => {
 | 
			
		||||
                        this.submitForm(e);
 | 
			
		||||
                    }}
 | 
			
		||||
                >
 | 
			
		||||
                    <ak-form-static
 | 
			
		||||
                        class="pf-c-form__group"
 | 
			
		||||
                        userAvatar="${this.challenge.pendingUserAvatar}"
 | 
			
		||||
                        user=${this.challenge.pendingUser}>
 | 
			
		||||
                        user=${this.challenge.pendingUser}
 | 
			
		||||
                    >
 | 
			
		||||
                        <div slot="link">
 | 
			
		||||
                            <a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}">${t`Not you?`}</a>
 | 
			
		||||
                            <a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
 | 
			
		||||
                                >${t`Not you?`}</a
 | 
			
		||||
                            >
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </ak-form-static>
 | 
			
		||||
                    <ak-form-element
 | 
			
		||||
                        label="${t`Tokens`}"
 | 
			
		||||
                        ?required="${true}"
 | 
			
		||||
                        class="pf-c-form__group">
 | 
			
		||||
                        class="pf-c-form__group"
 | 
			
		||||
                    >
 | 
			
		||||
                        <ul class="ak-otp-tokens">
 | 
			
		||||
                            ${this.challenge.codes.map((token) => {
 | 
			
		||||
                                return html`<li>${token}</li>`;
 | 
			
		||||
@ -78,9 +95,7 @@ export class AuthenticatorStaticStage extends BaseStage<AuthenticatorStaticChall
 | 
			
		||||
                </form>
 | 
			
		||||
            </div>
 | 
			
		||||
            <footer class="pf-c-login__main-footer">
 | 
			
		||||
                <ul class="pf-c-login__main-footer-links">
 | 
			
		||||
                </ul>
 | 
			
		||||
                <ul class="pf-c-login__main-footer-links"></ul>
 | 
			
		||||
            </footer>`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -14,53 +14,66 @@ import { showMessage } from "../../../elements/messages/MessageContainer";
 | 
			
		||||
import "../../../elements/EmptyState";
 | 
			
		||||
import "../../FormStatic";
 | 
			
		||||
import { MessageLevel } from "../../../elements/messages/Message";
 | 
			
		||||
import { AuthenticatorTOTPChallenge, AuthenticatorTOTPChallengeResponseRequest } from "authentik-api";
 | 
			
		||||
import {
 | 
			
		||||
    AuthenticatorTOTPChallenge,
 | 
			
		||||
    AuthenticatorTOTPChallengeResponseRequest,
 | 
			
		||||
} from "authentik-api";
 | 
			
		||||
import { ifDefined } from "lit-html/directives/if-defined";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@customElement("ak-stage-authenticator-totp")
 | 
			
		||||
export class AuthenticatorTOTPStage extends BaseStage<AuthenticatorTOTPChallenge, AuthenticatorTOTPChallengeResponseRequest> {
 | 
			
		||||
 | 
			
		||||
export class AuthenticatorTOTPStage extends BaseStage<
 | 
			
		||||
    AuthenticatorTOTPChallenge,
 | 
			
		||||
    AuthenticatorTOTPChallengeResponseRequest
 | 
			
		||||
> {
 | 
			
		||||
    static get styles(): CSSResult[] {
 | 
			
		||||
        return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render(): TemplateResult {
 | 
			
		||||
        if (!this.challenge) {
 | 
			
		||||
            return html`<ak-empty-state
 | 
			
		||||
                ?loading="${true}"
 | 
			
		||||
                header=${t`Loading`}>
 | 
			
		||||
            </ak-empty-state>`;
 | 
			
		||||
            return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
 | 
			
		||||
        }
 | 
			
		||||
        return html`<header class="pf-c-login__main-header">
 | 
			
		||||
                <h1 class="pf-c-title pf-m-3xl">
 | 
			
		||||
                    ${this.challenge.flowInfo?.title}
 | 
			
		||||
                </h1>
 | 
			
		||||
                <h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
 | 
			
		||||
            </header>
 | 
			
		||||
            <div class="pf-c-login__main-body">
 | 
			
		||||
                <form class="pf-c-form" @submit=${(e: Event) => { this.submitForm(e); }}>
 | 
			
		||||
                <form
 | 
			
		||||
                    class="pf-c-form"
 | 
			
		||||
                    @submit=${(e: Event) => {
 | 
			
		||||
                        this.submitForm(e);
 | 
			
		||||
                    }}
 | 
			
		||||
                >
 | 
			
		||||
                    <ak-form-static
 | 
			
		||||
                        class="pf-c-form__group"
 | 
			
		||||
                        userAvatar="${this.challenge.pendingUserAvatar}"
 | 
			
		||||
                        user=${this.challenge.pendingUser}>
 | 
			
		||||
                        user=${this.challenge.pendingUser}
 | 
			
		||||
                    >
 | 
			
		||||
                        <div slot="link">
 | 
			
		||||
                            <a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}">${t`Not you?`}</a>
 | 
			
		||||
                            <a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
 | 
			
		||||
                                >${t`Not you?`}</a
 | 
			
		||||
                            >
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </ak-form-static>
 | 
			
		||||
                    <input type="hidden" name="otp_uri" value=${this.challenge.configUrl} />
 | 
			
		||||
                    <ak-form-element>
 | 
			
		||||
                        <!-- @ts-ignore -->
 | 
			
		||||
                        <qr-code data="${this.challenge.configUrl}"></qr-code>
 | 
			
		||||
                        <button type="button" class="pf-c-button pf-m-secondary pf-m-progress pf-m-in-progress" @click=${(e: Event) => {
 | 
			
		||||
                            e.preventDefault();
 | 
			
		||||
                            if (!this.challenge?.configUrl) return;
 | 
			
		||||
                            navigator.clipboard.writeText(this.challenge?.configUrl).then(() => {
 | 
			
		||||
                                showMessage({
 | 
			
		||||
                                    level: MessageLevel.success,
 | 
			
		||||
                                    message: t`Successfully copied TOTP Config.`
 | 
			
		||||
                                });
 | 
			
		||||
                            });
 | 
			
		||||
                        }}>
 | 
			
		||||
                        <button
 | 
			
		||||
                            type="button"
 | 
			
		||||
                            class="pf-c-button pf-m-secondary pf-m-progress pf-m-in-progress"
 | 
			
		||||
                            @click=${(e: Event) => {
 | 
			
		||||
                                e.preventDefault();
 | 
			
		||||
                                if (!this.challenge?.configUrl) return;
 | 
			
		||||
                                navigator.clipboard
 | 
			
		||||
                                    .writeText(this.challenge?.configUrl)
 | 
			
		||||
                                    .then(() => {
 | 
			
		||||
                                        showMessage({
 | 
			
		||||
                                            level: MessageLevel.success,
 | 
			
		||||
                                            message: t`Successfully copied TOTP Config.`,
 | 
			
		||||
                                        });
 | 
			
		||||
                                    });
 | 
			
		||||
                            }}
 | 
			
		||||
                        >
 | 
			
		||||
                            <span class="pf-c-button__progress"><i class="fas fa-copy"></i></span>
 | 
			
		||||
                            ${t`Copy`}
 | 
			
		||||
                        </button>
 | 
			
		||||
@ -69,9 +82,11 @@ export class AuthenticatorTOTPStage extends BaseStage<AuthenticatorTOTPChallenge
 | 
			
		||||
                        label="${t`Code`}"
 | 
			
		||||
                        ?required="${true}"
 | 
			
		||||
                        class="pf-c-form__group"
 | 
			
		||||
                        .errors=${(this.challenge?.responseErrors || {})["code"]}>
 | 
			
		||||
                        .errors=${(this.challenge?.responseErrors || {})["code"]}
 | 
			
		||||
                    >
 | 
			
		||||
                        <!-- @ts-ignore -->
 | 
			
		||||
                        <input type="text"
 | 
			
		||||
                        <input
 | 
			
		||||
                            type="text"
 | 
			
		||||
                            name="code"
 | 
			
		||||
                            inputmode="numeric"
 | 
			
		||||
                            pattern="[0-9]*"
 | 
			
		||||
@ -79,7 +94,8 @@ export class AuthenticatorTOTPStage extends BaseStage<AuthenticatorTOTPChallenge
 | 
			
		||||
                            autofocus=""
 | 
			
		||||
                            autocomplete="one-time-code"
 | 
			
		||||
                            class="pf-c-form-control"
 | 
			
		||||
                            required>
 | 
			
		||||
                            required
 | 
			
		||||
                        />
 | 
			
		||||
                    </ak-form-element>
 | 
			
		||||
 | 
			
		||||
                    <div class="pf-c-form__group pf-m-action">
 | 
			
		||||
@ -90,9 +106,7 @@ export class AuthenticatorTOTPStage extends BaseStage<AuthenticatorTOTPChallenge
 | 
			
		||||
                </form>
 | 
			
		||||
            </div>
 | 
			
		||||
            <footer class="pf-c-login__main-footer">
 | 
			
		||||
                <ul class="pf-c-login__main-footer-links">
 | 
			
		||||
                </ul>
 | 
			
		||||
                <ul class="pf-c-login__main-footer-links"></ul>
 | 
			
		||||
            </footer>`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -12,7 +12,11 @@ import "./AuthenticatorValidateStageWebAuthn";
 | 
			
		||||
import "./AuthenticatorValidateStageCode";
 | 
			
		||||
import "./AuthenticatorValidateStageDuo";
 | 
			
		||||
import { PasswordManagerPrefill } from "../identification/IdentificationStage";
 | 
			
		||||
import { AuthenticatorValidationChallenge, AuthenticatorValidationChallengeResponseRequest, DeviceChallenge } from "authentik-api";
 | 
			
		||||
import {
 | 
			
		||||
    AuthenticatorValidationChallenge,
 | 
			
		||||
    AuthenticatorValidationChallengeResponseRequest,
 | 
			
		||||
    DeviceChallenge,
 | 
			
		||||
} from "authentik-api";
 | 
			
		||||
 | 
			
		||||
export enum DeviceClasses {
 | 
			
		||||
    STATIC = "static",
 | 
			
		||||
@ -22,9 +26,14 @@ export enum DeviceClasses {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@customElement("ak-stage-authenticator-validate")
 | 
			
		||||
export class AuthenticatorValidateStage extends BaseStage<AuthenticatorValidationChallenge, AuthenticatorValidationChallengeResponseRequest> implements StageHost {
 | 
			
		||||
 | 
			
		||||
    @property({attribute: false})
 | 
			
		||||
export class AuthenticatorValidateStage
 | 
			
		||||
    extends BaseStage<
 | 
			
		||||
        AuthenticatorValidationChallenge,
 | 
			
		||||
        AuthenticatorValidationChallengeResponseRequest
 | 
			
		||||
    >
 | 
			
		||||
    implements StageHost
 | 
			
		||||
{
 | 
			
		||||
    @property({ attribute: false })
 | 
			
		||||
    selectedDeviceChallenge?: DeviceChallenge;
 | 
			
		||||
 | 
			
		||||
    submit(payload: AuthenticatorValidationChallengeResponseRequest): Promise<void> {
 | 
			
		||||
@ -65,7 +74,9 @@ export class AuthenticatorValidateStage extends BaseStage<AuthenticatorValidatio
 | 
			
		||||
                return html`<i class="fas fa-mobile-alt"></i>
 | 
			
		||||
                    <div class="right">
 | 
			
		||||
                        <p>${t`Duo push-notifications`}</p>
 | 
			
		||||
                        <small>${t`Receive a push notification on your phone to prove your identity.`}</small>
 | 
			
		||||
                        <small
 | 
			
		||||
                            >${t`Receive a push notification on your phone to prove your identity.`}</small
 | 
			
		||||
                        >
 | 
			
		||||
                    </div>`;
 | 
			
		||||
            case DeviceClasses.WEBAUTHN:
 | 
			
		||||
                return html`<i class="fas fa-mobile-alt"></i>
 | 
			
		||||
@ -78,7 +89,9 @@ export class AuthenticatorValidateStage extends BaseStage<AuthenticatorValidatio
 | 
			
		||||
                // and we have a pre-filled value from the password manager,
 | 
			
		||||
                // directly set the the TOTP device Challenge as active.
 | 
			
		||||
                if (PasswordManagerPrefill.totp) {
 | 
			
		||||
                    console.debug("authentik/stages/authenticator_validate: found prefill totp code, selecting totp challenge");
 | 
			
		||||
                    console.debug(
 | 
			
		||||
                        "authentik/stages/authenticator_validate: found prefill totp code, selecting totp challenge",
 | 
			
		||||
                    );
 | 
			
		||||
                    this.selectedDeviceChallenge = deviceChallenge;
 | 
			
		||||
                    // Delay the update as a re-render isn't triggered from here
 | 
			
		||||
                    setTimeout(() => {
 | 
			
		||||
@ -103,13 +116,16 @@ export class AuthenticatorValidateStage extends BaseStage<AuthenticatorValidatio
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    renderDevicePicker(): TemplateResult {
 | 
			
		||||
        return html`
 | 
			
		||||
        <ul>
 | 
			
		||||
        return html` <ul>
 | 
			
		||||
            ${this.challenge?.deviceChallenges.map((challenges) => {
 | 
			
		||||
                return html`<li>
 | 
			
		||||
                    <button class="pf-c-button authenticator-button" type="button" @click=${() => {
 | 
			
		||||
                        this.selectedDeviceChallenge = challenges;
 | 
			
		||||
                    }}>
 | 
			
		||||
                    <button
 | 
			
		||||
                        class="pf-c-button authenticator-button"
 | 
			
		||||
                        type="button"
 | 
			
		||||
                        @click=${() => {
 | 
			
		||||
                            this.selectedDeviceChallenge = challenges;
 | 
			
		||||
                        }}
 | 
			
		||||
                    >
 | 
			
		||||
                        ${this.renderDevicePickerSingle(challenges)}
 | 
			
		||||
                    </button>
 | 
			
		||||
                </li>`;
 | 
			
		||||
@ -122,60 +138,56 @@ export class AuthenticatorValidateStage extends BaseStage<AuthenticatorValidatio
 | 
			
		||||
            return html``;
 | 
			
		||||
        }
 | 
			
		||||
        switch (this.selectedDeviceChallenge?.deviceClass) {
 | 
			
		||||
        case DeviceClasses.STATIC:
 | 
			
		||||
        case DeviceClasses.TOTP:
 | 
			
		||||
            return html`<ak-stage-authenticator-validate-code
 | 
			
		||||
                .host=${this}
 | 
			
		||||
                .challenge=${this.challenge}
 | 
			
		||||
                .deviceChallenge=${this.selectedDeviceChallenge}
 | 
			
		||||
                .showBackButton=${(this.challenge?.deviceChallenges.length || []) > 1}>
 | 
			
		||||
            </ak-stage-authenticator-validate-code>`;
 | 
			
		||||
        case DeviceClasses.WEBAUTHN:
 | 
			
		||||
            return html`<ak-stage-authenticator-validate-webauthn
 | 
			
		||||
                .host=${this}
 | 
			
		||||
                .challenge=${this.challenge}
 | 
			
		||||
                .deviceChallenge=${this.selectedDeviceChallenge}
 | 
			
		||||
                .showBackButton=${(this.challenge?.deviceChallenges.length || []) > 1}>
 | 
			
		||||
            </ak-stage-authenticator-validate-webauthn>`;
 | 
			
		||||
        case DeviceClasses.DUO:
 | 
			
		||||
            return html`<ak-stage-authenticator-validate-duo
 | 
			
		||||
                .host=${this}
 | 
			
		||||
                .challenge=${this.challenge}
 | 
			
		||||
                .deviceChallenge=${this.selectedDeviceChallenge}
 | 
			
		||||
                .showBackButton=${(this.challenge?.deviceChallenges.length || []) > 1}>
 | 
			
		||||
            </ak-stage-authenticator-validate-duo>`;
 | 
			
		||||
            case DeviceClasses.STATIC:
 | 
			
		||||
            case DeviceClasses.TOTP:
 | 
			
		||||
                return html`<ak-stage-authenticator-validate-code
 | 
			
		||||
                    .host=${this}
 | 
			
		||||
                    .challenge=${this.challenge}
 | 
			
		||||
                    .deviceChallenge=${this.selectedDeviceChallenge}
 | 
			
		||||
                    .showBackButton=${(this.challenge?.deviceChallenges.length || []) > 1}
 | 
			
		||||
                >
 | 
			
		||||
                </ak-stage-authenticator-validate-code>`;
 | 
			
		||||
            case DeviceClasses.WEBAUTHN:
 | 
			
		||||
                return html`<ak-stage-authenticator-validate-webauthn
 | 
			
		||||
                    .host=${this}
 | 
			
		||||
                    .challenge=${this.challenge}
 | 
			
		||||
                    .deviceChallenge=${this.selectedDeviceChallenge}
 | 
			
		||||
                    .showBackButton=${(this.challenge?.deviceChallenges.length || []) > 1}
 | 
			
		||||
                >
 | 
			
		||||
                </ak-stage-authenticator-validate-webauthn>`;
 | 
			
		||||
            case DeviceClasses.DUO:
 | 
			
		||||
                return html`<ak-stage-authenticator-validate-duo
 | 
			
		||||
                    .host=${this}
 | 
			
		||||
                    .challenge=${this.challenge}
 | 
			
		||||
                    .deviceChallenge=${this.selectedDeviceChallenge}
 | 
			
		||||
                    .showBackButton=${(this.challenge?.deviceChallenges.length || []) > 1}
 | 
			
		||||
                >
 | 
			
		||||
                </ak-stage-authenticator-validate-duo>`;
 | 
			
		||||
        }
 | 
			
		||||
        return html``;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render(): TemplateResult {
 | 
			
		||||
        if (!this.challenge) {
 | 
			
		||||
            return html`<ak-empty-state
 | 
			
		||||
                ?loading="${true}"
 | 
			
		||||
                header=${t`Loading`}>
 | 
			
		||||
            </ak-empty-state>`;
 | 
			
		||||
            return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
 | 
			
		||||
        }
 | 
			
		||||
        // User only has a single device class, so we don't show a picker
 | 
			
		||||
        if (this.challenge?.deviceChallenges.length === 1) {
 | 
			
		||||
            this.selectedDeviceChallenge = this.challenge.deviceChallenges[0];
 | 
			
		||||
        }
 | 
			
		||||
        return html`<header class="pf-c-login__main-header">
 | 
			
		||||
                <h1 class="pf-c-title pf-m-3xl">
 | 
			
		||||
                    ${this.challenge.flowInfo?.title}
 | 
			
		||||
                </h1>
 | 
			
		||||
                ${this.selectedDeviceChallenge ? "" : html`<p class="pf-c-login__main-header-desc">
 | 
			
		||||
                    ${t`Select an identification method.`}
 | 
			
		||||
                    </p>`}
 | 
			
		||||
                <h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
 | 
			
		||||
                ${this.selectedDeviceChallenge
 | 
			
		||||
                    ? ""
 | 
			
		||||
                    : html`<p class="pf-c-login__main-header-desc">
 | 
			
		||||
                          ${t`Select an identification method.`}
 | 
			
		||||
                      </p>`}
 | 
			
		||||
            </header>
 | 
			
		||||
            ${this.selectedDeviceChallenge ?
 | 
			
		||||
                this.renderDeviceChallenge() :
 | 
			
		||||
                html`<div class="pf-c-login__main-body">
 | 
			
		||||
                    ${this.renderDevicePicker()}
 | 
			
		||||
                </div>
 | 
			
		||||
                <footer class="pf-c-login__main-footer">
 | 
			
		||||
                    <ul class="pf-c-login__main-footer-links">
 | 
			
		||||
                    </ul>
 | 
			
		||||
                </footer>`}`;
 | 
			
		||||
            ${this.selectedDeviceChallenge
 | 
			
		||||
                ? this.renderDeviceChallenge()
 | 
			
		||||
                : html`<div class="pf-c-login__main-body">${this.renderDevicePicker()}</div>
 | 
			
		||||
                      <footer class="pf-c-login__main-footer">
 | 
			
		||||
                          <ul class="pf-c-login__main-footer-links"></ul>
 | 
			
		||||
                      </footer>`}`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -13,12 +13,18 @@ import "../../../elements/forms/FormElement";
 | 
			
		||||
import "../../../elements/EmptyState";
 | 
			
		||||
import { PasswordManagerPrefill } from "../identification/IdentificationStage";
 | 
			
		||||
import "../../FormStatic";
 | 
			
		||||
import { AuthenticatorValidationChallenge, AuthenticatorValidationChallengeResponseRequest, DeviceChallenge } from "authentik-api";
 | 
			
		||||
import {
 | 
			
		||||
    AuthenticatorValidationChallenge,
 | 
			
		||||
    AuthenticatorValidationChallengeResponseRequest,
 | 
			
		||||
    DeviceChallenge,
 | 
			
		||||
} from "authentik-api";
 | 
			
		||||
import { ifDefined } from "lit-html/directives/if-defined";
 | 
			
		||||
 | 
			
		||||
@customElement("ak-stage-authenticator-validate-code")
 | 
			
		||||
export class AuthenticatorValidateStageWebCode extends BaseStage<AuthenticatorValidationChallenge, AuthenticatorValidationChallengeResponseRequest> {
 | 
			
		||||
 | 
			
		||||
export class AuthenticatorValidateStageWebCode extends BaseStage<
 | 
			
		||||
    AuthenticatorValidationChallenge,
 | 
			
		||||
    AuthenticatorValidationChallengeResponseRequest
 | 
			
		||||
> {
 | 
			
		||||
    @property({ attribute: false })
 | 
			
		||||
    deviceChallenge?: DeviceChallenge;
 | 
			
		||||
 | 
			
		||||
@ -31,60 +37,72 @@ export class AuthenticatorValidateStageWebCode extends BaseStage<AuthenticatorVa
 | 
			
		||||
 | 
			
		||||
    render(): TemplateResult {
 | 
			
		||||
        if (!this.challenge) {
 | 
			
		||||
            return html`<ak-empty-state
 | 
			
		||||
                ?loading="${true}"
 | 
			
		||||
                header=${t`Loading`}>
 | 
			
		||||
            </ak-empty-state>`;
 | 
			
		||||
            return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
 | 
			
		||||
        }
 | 
			
		||||
        return html`<div class="pf-c-login__main-body">
 | 
			
		||||
            <form class="pf-c-form" @submit=${(e: Event) => { this.submitForm(e); }}>
 | 
			
		||||
                <ak-form-static
 | 
			
		||||
                    class="pf-c-form__group"
 | 
			
		||||
                    userAvatar="${this.challenge.pendingUserAvatar}"
 | 
			
		||||
                    user=${this.challenge.pendingUser}>
 | 
			
		||||
                    <div slot="link">
 | 
			
		||||
                        <a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}">${t`Not you?`}</a>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </ak-form-static>
 | 
			
		||||
                <ak-form-element
 | 
			
		||||
                    label="${t`Code`}"
 | 
			
		||||
                    ?required="${true}"
 | 
			
		||||
                    class="pf-c-form__group"
 | 
			
		||||
                    .errors=${(this.challenge?.responseErrors || {})["code"]}>
 | 
			
		||||
                    <!-- @ts-ignore -->
 | 
			
		||||
                    <input type="text"
 | 
			
		||||
                        name="code"
 | 
			
		||||
                        inputmode="numeric"
 | 
			
		||||
                        pattern="[0-9]*"
 | 
			
		||||
                        placeholder="${t`Please enter your TOTP Code`}"
 | 
			
		||||
                        autofocus=""
 | 
			
		||||
                        autocomplete="one-time-code"
 | 
			
		||||
                        class="pf-c-form-control"
 | 
			
		||||
                        value="${PasswordManagerPrefill.totp || ""}"
 | 
			
		||||
                        required>
 | 
			
		||||
                </ak-form-element>
 | 
			
		||||
                <form
 | 
			
		||||
                    class="pf-c-form"
 | 
			
		||||
                    @submit=${(e: Event) => {
 | 
			
		||||
                        this.submitForm(e);
 | 
			
		||||
                    }}
 | 
			
		||||
                >
 | 
			
		||||
                    <ak-form-static
 | 
			
		||||
                        class="pf-c-form__group"
 | 
			
		||||
                        userAvatar="${this.challenge.pendingUserAvatar}"
 | 
			
		||||
                        user=${this.challenge.pendingUser}
 | 
			
		||||
                    >
 | 
			
		||||
                        <div slot="link">
 | 
			
		||||
                            <a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
 | 
			
		||||
                                >${t`Not you?`}</a
 | 
			
		||||
                            >
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </ak-form-static>
 | 
			
		||||
                    <ak-form-element
 | 
			
		||||
                        label="${t`Code`}"
 | 
			
		||||
                        ?required="${true}"
 | 
			
		||||
                        class="pf-c-form__group"
 | 
			
		||||
                        .errors=${(this.challenge?.responseErrors || {})["code"]}
 | 
			
		||||
                    >
 | 
			
		||||
                        <!-- @ts-ignore -->
 | 
			
		||||
                        <input
 | 
			
		||||
                            type="text"
 | 
			
		||||
                            name="code"
 | 
			
		||||
                            inputmode="numeric"
 | 
			
		||||
                            pattern="[0-9]*"
 | 
			
		||||
                            placeholder="${t`Please enter your TOTP Code`}"
 | 
			
		||||
                            autofocus=""
 | 
			
		||||
                            autocomplete="one-time-code"
 | 
			
		||||
                            class="pf-c-form-control"
 | 
			
		||||
                            value="${PasswordManagerPrefill.totp || ""}"
 | 
			
		||||
                            required
 | 
			
		||||
                        />
 | 
			
		||||
                    </ak-form-element>
 | 
			
		||||
 | 
			
		||||
                <div class="pf-c-form__group pf-m-action">
 | 
			
		||||
                    <button type="submit" class="pf-c-button pf-m-primary pf-m-block">
 | 
			
		||||
                        ${t`Continue`}
 | 
			
		||||
                    </button>
 | 
			
		||||
                </div>
 | 
			
		||||
            </form>
 | 
			
		||||
        </div>
 | 
			
		||||
        <footer class="pf-c-login__main-footer">
 | 
			
		||||
            <ul class="pf-c-login__main-footer-links">
 | 
			
		||||
                ${this.showBackButton ?
 | 
			
		||||
                    html`<li class="pf-c-login__main-footer-links-item">
 | 
			
		||||
                        <button class="pf-c-button pf-m-secondary pf-m-block" @click=${() => {
 | 
			
		||||
                            if (!this.host) return;
 | 
			
		||||
                            (this.host as AuthenticatorValidateStage).selectedDeviceChallenge = undefined;
 | 
			
		||||
                        }}>
 | 
			
		||||
                            ${t`Return to device picker`}
 | 
			
		||||
                    <div class="pf-c-form__group pf-m-action">
 | 
			
		||||
                        <button type="submit" class="pf-c-button pf-m-primary pf-m-block">
 | 
			
		||||
                            ${t`Continue`}
 | 
			
		||||
                        </button>
 | 
			
		||||
                    </li>`:
 | 
			
		||||
                    html``}
 | 
			
		||||
            </ul>
 | 
			
		||||
        </footer>`;
 | 
			
		||||
                    </div>
 | 
			
		||||
                </form>
 | 
			
		||||
            </div>
 | 
			
		||||
            <footer class="pf-c-login__main-footer">
 | 
			
		||||
                <ul class="pf-c-login__main-footer-links">
 | 
			
		||||
                    ${this.showBackButton
 | 
			
		||||
                        ? html`<li class="pf-c-login__main-footer-links-item">
 | 
			
		||||
                              <button
 | 
			
		||||
                                  class="pf-c-button pf-m-secondary pf-m-block"
 | 
			
		||||
                                  @click=${() => {
 | 
			
		||||
                                      if (!this.host) return;
 | 
			
		||||
                                      (
 | 
			
		||||
                                          this.host as AuthenticatorValidateStage
 | 
			
		||||
                                      ).selectedDeviceChallenge = undefined;
 | 
			
		||||
                                  }}
 | 
			
		||||
                              >
 | 
			
		||||
                                  ${t`Return to device picker`}
 | 
			
		||||
                              </button>
 | 
			
		||||
                          </li>`
 | 
			
		||||
                        : html``}
 | 
			
		||||
                </ul>
 | 
			
		||||
            </footer>`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -12,12 +12,18 @@ import { AuthenticatorValidateStage } from "./AuthenticatorValidateStage";
 | 
			
		||||
import "../../../elements/forms/FormElement";
 | 
			
		||||
import "../../../elements/EmptyState";
 | 
			
		||||
import "../../FormStatic";
 | 
			
		||||
import { AuthenticatorValidationChallenge, AuthenticatorValidationChallengeResponseRequest, DeviceChallenge } from "authentik-api";
 | 
			
		||||
import {
 | 
			
		||||
    AuthenticatorValidationChallenge,
 | 
			
		||||
    AuthenticatorValidationChallengeResponseRequest,
 | 
			
		||||
    DeviceChallenge,
 | 
			
		||||
} from "authentik-api";
 | 
			
		||||
import { ifDefined } from "lit-html/directives/if-defined";
 | 
			
		||||
 | 
			
		||||
@customElement("ak-stage-authenticator-validate-duo")
 | 
			
		||||
export class AuthenticatorValidateStageWebDuo extends BaseStage<AuthenticatorValidationChallenge, AuthenticatorValidationChallengeResponseRequest> {
 | 
			
		||||
 | 
			
		||||
export class AuthenticatorValidateStageWebDuo extends BaseStage<
 | 
			
		||||
    AuthenticatorValidationChallenge,
 | 
			
		||||
    AuthenticatorValidationChallengeResponseRequest
 | 
			
		||||
> {
 | 
			
		||||
    @property({ attribute: false })
 | 
			
		||||
    deviceChallenge?: DeviceChallenge;
 | 
			
		||||
 | 
			
		||||
@ -30,49 +36,58 @@ export class AuthenticatorValidateStageWebDuo extends BaseStage<AuthenticatorVal
 | 
			
		||||
 | 
			
		||||
    firstUpdated(): void {
 | 
			
		||||
        this.host?.submit({
 | 
			
		||||
            "duo": this.deviceChallenge?.deviceUid
 | 
			
		||||
            duo: this.deviceChallenge?.deviceUid,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render(): TemplateResult {
 | 
			
		||||
        if (!this.challenge) {
 | 
			
		||||
            return html`<ak-empty-state
 | 
			
		||||
                ?loading="${true}"
 | 
			
		||||
                header=${t`Loading`}>
 | 
			
		||||
            </ak-empty-state>`;
 | 
			
		||||
            return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
 | 
			
		||||
        }
 | 
			
		||||
        return html`<div class="pf-c-login__main-body">
 | 
			
		||||
            <form class="pf-c-form" @submit=${(e: Event) => { this.submitForm(e); }}>
 | 
			
		||||
                <ak-form-static
 | 
			
		||||
                    class="pf-c-form__group"
 | 
			
		||||
                    userAvatar="${this.challenge.pendingUserAvatar}"
 | 
			
		||||
                    user=${this.challenge.pendingUser}>
 | 
			
		||||
                    <div slot="link">
 | 
			
		||||
                        <a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}">${t`Not you?`}</a>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </ak-form-static>
 | 
			
		||||
                <form
 | 
			
		||||
                    class="pf-c-form"
 | 
			
		||||
                    @submit=${(e: Event) => {
 | 
			
		||||
                        this.submitForm(e);
 | 
			
		||||
                    }}
 | 
			
		||||
                >
 | 
			
		||||
                    <ak-form-static
 | 
			
		||||
                        class="pf-c-form__group"
 | 
			
		||||
                        userAvatar="${this.challenge.pendingUserAvatar}"
 | 
			
		||||
                        user=${this.challenge.pendingUser}
 | 
			
		||||
                    >
 | 
			
		||||
                        <div slot="link">
 | 
			
		||||
                            <a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
 | 
			
		||||
                                >${t`Not you?`}</a
 | 
			
		||||
                            >
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </ak-form-static>
 | 
			
		||||
 | 
			
		||||
                <div class="pf-c-form__group pf-m-action">
 | 
			
		||||
                    <button type="submit" class="pf-c-button pf-m-primary pf-m-block">
 | 
			
		||||
                        ${t`Continue`}
 | 
			
		||||
                    </button>
 | 
			
		||||
                </div>
 | 
			
		||||
            </form>
 | 
			
		||||
        </div>
 | 
			
		||||
        <footer class="pf-c-login__main-footer">
 | 
			
		||||
            <ul class="pf-c-login__main-footer-links">
 | 
			
		||||
                ${this.showBackButton ?
 | 
			
		||||
                    html`<li class="pf-c-login__main-footer-links-item">
 | 
			
		||||
                        <button class="pf-c-button pf-m-secondary pf-m-block" @click=${() => {
 | 
			
		||||
                            if (!this.host) return;
 | 
			
		||||
                            (this.host as AuthenticatorValidateStage).selectedDeviceChallenge = undefined;
 | 
			
		||||
                        }}>
 | 
			
		||||
                            ${t`Return to device picker`}
 | 
			
		||||
                    <div class="pf-c-form__group pf-m-action">
 | 
			
		||||
                        <button type="submit" class="pf-c-button pf-m-primary pf-m-block">
 | 
			
		||||
                            ${t`Continue`}
 | 
			
		||||
                        </button>
 | 
			
		||||
                    </li>`:
 | 
			
		||||
                    html``}
 | 
			
		||||
            </ul>
 | 
			
		||||
        </footer>`;
 | 
			
		||||
                    </div>
 | 
			
		||||
                </form>
 | 
			
		||||
            </div>
 | 
			
		||||
            <footer class="pf-c-login__main-footer">
 | 
			
		||||
                <ul class="pf-c-login__main-footer-links">
 | 
			
		||||
                    ${this.showBackButton
 | 
			
		||||
                        ? html`<li class="pf-c-login__main-footer-links-item">
 | 
			
		||||
                              <button
 | 
			
		||||
                                  class="pf-c-button pf-m-secondary pf-m-block"
 | 
			
		||||
                                  @click=${() => {
 | 
			
		||||
                                      if (!this.host) return;
 | 
			
		||||
                                      (
 | 
			
		||||
                                          this.host as AuthenticatorValidateStage
 | 
			
		||||
                                      ).selectedDeviceChallenge = undefined;
 | 
			
		||||
                                  }}
 | 
			
		||||
                              >
 | 
			
		||||
                                  ${t`Return to device picker`}
 | 
			
		||||
                              </button>
 | 
			
		||||
                          </li>`
 | 
			
		||||
                        : html``}
 | 
			
		||||
                </ul>
 | 
			
		||||
            </footer>`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -8,15 +8,24 @@ import PFButton from "@patternfly/patternfly/components/Button/button.css";
 | 
			
		||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
 | 
			
		||||
import AKGlobal from "../../../authentik.css";
 | 
			
		||||
import { PFSize } from "../../../elements/Spinner";
 | 
			
		||||
import { transformAssertionForServer, transformCredentialRequestOptions } from "../authenticator_webauthn/utils";
 | 
			
		||||
import {
 | 
			
		||||
    transformAssertionForServer,
 | 
			
		||||
    transformCredentialRequestOptions,
 | 
			
		||||
} from "../authenticator_webauthn/utils";
 | 
			
		||||
import { BaseStage } from "../base";
 | 
			
		||||
import { AuthenticatorValidateStage } from "./AuthenticatorValidateStage";
 | 
			
		||||
import { AuthenticatorValidationChallenge, AuthenticatorValidationChallengeResponseRequest, DeviceChallenge } from "authentik-api";
 | 
			
		||||
import {
 | 
			
		||||
    AuthenticatorValidationChallenge,
 | 
			
		||||
    AuthenticatorValidationChallengeResponseRequest,
 | 
			
		||||
    DeviceChallenge,
 | 
			
		||||
} from "authentik-api";
 | 
			
		||||
 | 
			
		||||
@customElement("ak-stage-authenticator-validate-webauthn")
 | 
			
		||||
export class AuthenticatorValidateStageWebAuthn extends BaseStage<AuthenticatorValidationChallenge, AuthenticatorValidationChallengeResponseRequest> {
 | 
			
		||||
 | 
			
		||||
    @property({attribute: false})
 | 
			
		||||
export class AuthenticatorValidateStageWebAuthn extends BaseStage<
 | 
			
		||||
    AuthenticatorValidationChallenge,
 | 
			
		||||
    AuthenticatorValidationChallengeResponseRequest
 | 
			
		||||
> {
 | 
			
		||||
    @property({ attribute: false })
 | 
			
		||||
    deviceChallenge?: DeviceChallenge;
 | 
			
		||||
 | 
			
		||||
    @property({ type: Boolean })
 | 
			
		||||
@ -25,7 +34,7 @@ export class AuthenticatorValidateStageWebAuthn extends BaseStage<AuthenticatorV
 | 
			
		||||
    @property()
 | 
			
		||||
    authenticateMessage = "";
 | 
			
		||||
 | 
			
		||||
    @property({type: Boolean})
 | 
			
		||||
    @property({ type: Boolean })
 | 
			
		||||
    showBackButton = false;
 | 
			
		||||
 | 
			
		||||
    static get styles(): CSSResult[] {
 | 
			
		||||
@ -35,8 +44,11 @@ export class AuthenticatorValidateStageWebAuthn extends BaseStage<AuthenticatorV
 | 
			
		||||
    async authenticate(): Promise<void> {
 | 
			
		||||
        // convert certain members of the PublicKeyCredentialRequestOptions into
 | 
			
		||||
        // byte arrays as expected by the spec.
 | 
			
		||||
        const credentialRequestOptions = <PublicKeyCredentialRequestOptions>this.deviceChallenge?.challenge;
 | 
			
		||||
        const transformedCredentialRequestOptions = transformCredentialRequestOptions(credentialRequestOptions);
 | 
			
		||||
        const credentialRequestOptions = <PublicKeyCredentialRequestOptions>(
 | 
			
		||||
            this.deviceChallenge?.challenge
 | 
			
		||||
        );
 | 
			
		||||
        const transformedCredentialRequestOptions =
 | 
			
		||||
            transformCredentialRequestOptions(credentialRequestOptions);
 | 
			
		||||
 | 
			
		||||
        // request the authenticator to create an assertion signature using the
 | 
			
		||||
        // credential private key
 | 
			
		||||
@ -54,12 +66,14 @@ export class AuthenticatorValidateStageWebAuthn extends BaseStage<AuthenticatorV
 | 
			
		||||
 | 
			
		||||
        // 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(<PublicKeyCredential>assertion);
 | 
			
		||||
        const transformedAssertionForServer = transformAssertionForServer(
 | 
			
		||||
            <PublicKeyCredential>assertion,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // post the assertion to the server for verification.
 | 
			
		||||
        try {
 | 
			
		||||
            await this.host?.submit({
 | 
			
		||||
                webauthn: transformedAssertionForServer
 | 
			
		||||
                webauthn: transformedAssertionForServer,
 | 
			
		||||
            });
 | 
			
		||||
        } catch (err) {
 | 
			
		||||
            throw new Error(t`Error when validating assertion on server: ${err}`);
 | 
			
		||||
@ -75,48 +89,56 @@ export class AuthenticatorValidateStageWebAuthn extends BaseStage<AuthenticatorV
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        this.authenticateRunning = true;
 | 
			
		||||
        this.authenticate().catch((e) => {
 | 
			
		||||
            console.error(e);
 | 
			
		||||
            this.authenticateMessage = e.toString();
 | 
			
		||||
        }).finally(() => {
 | 
			
		||||
            this.authenticateRunning = false;
 | 
			
		||||
        });
 | 
			
		||||
        this.authenticate()
 | 
			
		||||
            .catch((e) => {
 | 
			
		||||
                console.error(e);
 | 
			
		||||
                this.authenticateMessage = e.toString();
 | 
			
		||||
            })
 | 
			
		||||
            .finally(() => {
 | 
			
		||||
                this.authenticateRunning = false;
 | 
			
		||||
            });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render(): TemplateResult {
 | 
			
		||||
        return html`<div class="pf-c-login__main-body">
 | 
			
		||||
            ${this.authenticateRunning ?
 | 
			
		||||
                html`<div class="pf-c-empty-state__content">
 | 
			
		||||
                        <div class="pf-l-bullseye">
 | 
			
		||||
                            <div class="pf-l-bullseye__item">
 | 
			
		||||
                                <ak-spinner size="${PFSize.XLarge}"></ak-spinner>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>`:
 | 
			
		||||
                html`
 | 
			
		||||
                <div class="pf-c-form__group pf-m-action">
 | 
			
		||||
                    <p class="pf-m-block">${this.authenticateMessage}</p>
 | 
			
		||||
                    <button class="pf-c-button pf-m-primary pf-m-block" @click=${() => {
 | 
			
		||||
                        this.authenticateWrapper();
 | 
			
		||||
                    }}>
 | 
			
		||||
                        ${t`Retry authentication`}
 | 
			
		||||
                    </button>
 | 
			
		||||
                </div>`}
 | 
			
		||||
        </div>
 | 
			
		||||
        <footer class="pf-c-login__main-footer">
 | 
			
		||||
            <ul class="pf-c-login__main-footer-links">
 | 
			
		||||
                ${this.showBackButton ?
 | 
			
		||||
                    html`<li class="pf-c-login__main-footer-links-item">
 | 
			
		||||
                        <button class="pf-c-button pf-m-secondary pf-m-block" @click=${() => {
 | 
			
		||||
                            if (!this.host) return;
 | 
			
		||||
                            (this.host as AuthenticatorValidateStage).selectedDeviceChallenge = undefined;
 | 
			
		||||
                        }}>
 | 
			
		||||
                            ${t`Return to device picker`}
 | 
			
		||||
                        </button>
 | 
			
		||||
                    </li>`:
 | 
			
		||||
                    html``}
 | 
			
		||||
            </ul>
 | 
			
		||||
        </footer>`;
 | 
			
		||||
                ${this.authenticateRunning
 | 
			
		||||
                    ? html`<div class="pf-c-empty-state__content">
 | 
			
		||||
                          <div class="pf-l-bullseye">
 | 
			
		||||
                              <div class="pf-l-bullseye__item">
 | 
			
		||||
                                  <ak-spinner size="${PFSize.XLarge}"></ak-spinner>
 | 
			
		||||
                              </div>
 | 
			
		||||
                          </div>
 | 
			
		||||
                      </div>`
 | 
			
		||||
                    : html` <div class="pf-c-form__group pf-m-action">
 | 
			
		||||
                          <p class="pf-m-block">${this.authenticateMessage}</p>
 | 
			
		||||
                          <button
 | 
			
		||||
                              class="pf-c-button pf-m-primary pf-m-block"
 | 
			
		||||
                              @click=${() => {
 | 
			
		||||
                                  this.authenticateWrapper();
 | 
			
		||||
                              }}
 | 
			
		||||
                          >
 | 
			
		||||
                              ${t`Retry authentication`}
 | 
			
		||||
                          </button>
 | 
			
		||||
                      </div>`}
 | 
			
		||||
            </div>
 | 
			
		||||
            <footer class="pf-c-login__main-footer">
 | 
			
		||||
                <ul class="pf-c-login__main-footer-links">
 | 
			
		||||
                    ${this.showBackButton
 | 
			
		||||
                        ? html`<li class="pf-c-login__main-footer-links-item">
 | 
			
		||||
                              <button
 | 
			
		||||
                                  class="pf-c-button pf-m-secondary pf-m-block"
 | 
			
		||||
                                  @click=${() => {
 | 
			
		||||
                                      if (!this.host) return;
 | 
			
		||||
                                      (
 | 
			
		||||
                                          this.host as AuthenticatorValidateStage
 | 
			
		||||
                                      ).selectedDeviceChallenge = undefined;
 | 
			
		||||
                                  }}
 | 
			
		||||
                              >
 | 
			
		||||
                                  ${t`Return to device picker`}
 | 
			
		||||
                              </button>
 | 
			
		||||
                          </li>`
 | 
			
		||||
                        : html``}
 | 
			
		||||
                </ul>
 | 
			
		||||
            </footer>`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -9,17 +9,26 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
 | 
			
		||||
import AKGlobal from "../../../authentik.css";
 | 
			
		||||
import { PFSize } from "../../../elements/Spinner";
 | 
			
		||||
import { BaseStage } from "../base";
 | 
			
		||||
import { Assertion, transformCredentialCreateOptions, transformNewAssertionForServer } from "./utils";
 | 
			
		||||
import { AuthenticatorWebAuthnChallenge, AuthenticatorWebAuthnChallengeResponseRequest } from "authentik-api";
 | 
			
		||||
import {
 | 
			
		||||
    Assertion,
 | 
			
		||||
    transformCredentialCreateOptions,
 | 
			
		||||
    transformNewAssertionForServer,
 | 
			
		||||
} from "./utils";
 | 
			
		||||
import {
 | 
			
		||||
    AuthenticatorWebAuthnChallenge,
 | 
			
		||||
    AuthenticatorWebAuthnChallengeResponseRequest,
 | 
			
		||||
} from "authentik-api";
 | 
			
		||||
 | 
			
		||||
export interface WebAuthnAuthenticatorRegisterChallengeResponse {
 | 
			
		||||
    response: Assertion;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@customElement("ak-stage-authenticator-webauthn")
 | 
			
		||||
export class WebAuthnAuthenticatorRegisterStage extends BaseStage<AuthenticatorWebAuthnChallenge, AuthenticatorWebAuthnChallengeResponseRequest> {
 | 
			
		||||
 | 
			
		||||
    @property({type: Boolean})
 | 
			
		||||
export class WebAuthnAuthenticatorRegisterStage extends BaseStage<
 | 
			
		||||
    AuthenticatorWebAuthnChallenge,
 | 
			
		||||
    AuthenticatorWebAuthnChallengeResponseRequest
 | 
			
		||||
> {
 | 
			
		||||
    @property({ type: Boolean })
 | 
			
		||||
    registerRunning = false;
 | 
			
		||||
 | 
			
		||||
    @property()
 | 
			
		||||
@ -35,13 +44,15 @@ export class WebAuthnAuthenticatorRegisterStage extends BaseStage<AuthenticatorW
 | 
			
		||||
        }
 | 
			
		||||
        // convert certain members of the PublicKeyCredentialCreateOptions into
 | 
			
		||||
        // byte arrays as expected by the spec.
 | 
			
		||||
        const publicKeyCredentialCreateOptions = transformCredentialCreateOptions(this.challenge?.registration as PublicKeyCredentialCreationOptions);
 | 
			
		||||
        const publicKeyCredentialCreateOptions = transformCredentialCreateOptions(
 | 
			
		||||
            this.challenge?.registration as PublicKeyCredentialCreationOptions,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // request the authenticator(s) to create a new credential keypair.
 | 
			
		||||
        let credential;
 | 
			
		||||
        try {
 | 
			
		||||
            credential = <PublicKeyCredential> await navigator.credentials.create({
 | 
			
		||||
                publicKey: publicKeyCredentialCreateOptions
 | 
			
		||||
            credential = <PublicKeyCredential>await navigator.credentials.create({
 | 
			
		||||
                publicKey: publicKeyCredentialCreateOptions,
 | 
			
		||||
            });
 | 
			
		||||
            if (!credential) {
 | 
			
		||||
                throw new Error("Credential is empty");
 | 
			
		||||
@ -58,7 +69,7 @@ export class WebAuthnAuthenticatorRegisterStage extends BaseStage<AuthenticatorW
 | 
			
		||||
        // and storing the public key
 | 
			
		||||
        try {
 | 
			
		||||
            await this.host?.submit({
 | 
			
		||||
                response: newAssertionForServer
 | 
			
		||||
                response: newAssertionForServer,
 | 
			
		||||
            });
 | 
			
		||||
        } catch (err) {
 | 
			
		||||
            throw new Error(t`Server validation of credential failed: ${err}`);
 | 
			
		||||
@ -70,12 +81,14 @@ export class WebAuthnAuthenticatorRegisterStage extends BaseStage<AuthenticatorW
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        this.registerRunning = true;
 | 
			
		||||
        this.register().catch((e) => {
 | 
			
		||||
            console.error(e);
 | 
			
		||||
            this.registerMessage = e.toString();
 | 
			
		||||
        }).finally(() => {
 | 
			
		||||
            this.registerRunning = false;
 | 
			
		||||
        });
 | 
			
		||||
        this.register()
 | 
			
		||||
            .catch((e) => {
 | 
			
		||||
                console.error(e);
 | 
			
		||||
                this.registerMessage = e.toString();
 | 
			
		||||
            })
 | 
			
		||||
            .finally(() => {
 | 
			
		||||
                this.registerRunning = false;
 | 
			
		||||
            });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    firstUpdated(): void {
 | 
			
		||||
@ -89,26 +102,32 @@ export class WebAuthnAuthenticatorRegisterStage extends BaseStage<AuthenticatorW
 | 
			
		||||
                </h1>
 | 
			
		||||
            </header>
 | 
			
		||||
            <div class="pf-c-login__main-body">
 | 
			
		||||
                ${this.registerRunning ?
 | 
			
		||||
                    html`<div class="pf-c-empty-state__content">
 | 
			
		||||
                            <div class="pf-l-bullseye">
 | 
			
		||||
                                <div class="pf-l-bullseye__item">
 | 
			
		||||
                                    <ak-spinner size="${PFSize.XLarge}"></ak-spinner>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>`:
 | 
			
		||||
                    html`
 | 
			
		||||
                    <div class="pf-c-form__group pf-m-action">
 | 
			
		||||
                        ${this.challenge?.responseErrors ?
 | 
			
		||||
                            html`<p class="pf-m-block">${this.challenge.responseErrors["response"][0].string}</p>`:
 | 
			
		||||
                            html``}
 | 
			
		||||
                        <p class="pf-m-block">${this.registerMessage}</p>
 | 
			
		||||
                        <button class="pf-c-button pf-m-primary pf-m-block" @click=${() => {
 | 
			
		||||
                            this.registerWrapper();
 | 
			
		||||
                        }}>
 | 
			
		||||
                            ${t`Register device`}
 | 
			
		||||
                        </button>
 | 
			
		||||
                    </div>`}
 | 
			
		||||
                ${
 | 
			
		||||
                    this.registerRunning
 | 
			
		||||
                        ? html`<div class="pf-c-empty-state__content">
 | 
			
		||||
                              <div class="pf-l-bullseye">
 | 
			
		||||
                                  <div class="pf-l-bullseye__item">
 | 
			
		||||
                                      <ak-spinner size="${PFSize.XLarge}"></ak-spinner>
 | 
			
		||||
                                  </div>
 | 
			
		||||
                              </div>
 | 
			
		||||
                          </div>`
 | 
			
		||||
                        : html` <div class="pf-c-form__group pf-m-action">
 | 
			
		||||
                              ${this.challenge?.responseErrors
 | 
			
		||||
                                  ? html`<p class="pf-m-block">
 | 
			
		||||
                                        ${this.challenge.responseErrors["response"][0].string}
 | 
			
		||||
                                    </p>`
 | 
			
		||||
                                  : html``}
 | 
			
		||||
                              <p class="pf-m-block">${this.registerMessage}</p>
 | 
			
		||||
                              <button
 | 
			
		||||
                                  class="pf-c-button pf-m-primary pf-m-block"
 | 
			
		||||
                                  @click=${() => {
 | 
			
		||||
                                      this.registerWrapper();
 | 
			
		||||
                                  }}
 | 
			
		||||
                              >
 | 
			
		||||
                                  ${t`Register device`}
 | 
			
		||||
                              </button>
 | 
			
		||||
                          </div>`
 | 
			
		||||
                }
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <footer class="pf-c-login__main-footer">
 | 
			
		||||
@ -116,5 +135,4 @@ export class WebAuthnAuthenticatorRegisterStage extends BaseStage<AuthenticatorW
 | 
			
		||||
            </ul>
 | 
			
		||||
        </footer>`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -2,30 +2,28 @@ import * as base64js from "base64-js";
 | 
			
		||||
import { hexEncode } from "../../../utils";
 | 
			
		||||
 | 
			
		||||
export function b64enc(buf: Uint8Array): string {
 | 
			
		||||
    return base64js.fromByteArray(buf)
 | 
			
		||||
        .replace(/\+/g, "-")
 | 
			
		||||
        .replace(/\//g, "_")
 | 
			
		||||
        .replace(/=/g, "");
 | 
			
		||||
    return base64js.fromByteArray(buf).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function b64RawEnc(buf: Uint8Array): string {
 | 
			
		||||
    return base64js.fromByteArray(buf)
 | 
			
		||||
        .replace(/\+/g, "-")
 | 
			
		||||
        .replace(/\//g, "_");
 | 
			
		||||
    return base64js.fromByteArray(buf).replace(/\+/g, "-").replace(/\//g, "_");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Transforms items in the credentialCreateOptions generated on the server
 | 
			
		||||
 * into byte arrays expected by the navigator.credentials.create() call
 | 
			
		||||
 */
 | 
			
		||||
export function transformCredentialCreateOptions(credentialCreateOptions: PublicKeyCredentialCreationOptions): PublicKeyCredentialCreationOptions {
 | 
			
		||||
export function transformCredentialCreateOptions(
 | 
			
		||||
    credentialCreateOptions: PublicKeyCredentialCreationOptions,
 | 
			
		||||
): PublicKeyCredentialCreationOptions {
 | 
			
		||||
    const user = credentialCreateOptions.user;
 | 
			
		||||
    user.id = u8arr(b64enc(credentialCreateOptions.user.id as Uint8Array));
 | 
			
		||||
    const challenge = u8arr(credentialCreateOptions.challenge.toString());
 | 
			
		||||
 | 
			
		||||
    const transformedCredentialCreateOptions = Object.assign(
 | 
			
		||||
        {}, credentialCreateOptions,
 | 
			
		||||
        { challenge, user });
 | 
			
		||||
    const transformedCredentialCreateOptions = Object.assign({}, credentialCreateOptions, {
 | 
			
		||||
        challenge,
 | 
			
		||||
        user,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return transformedCredentialCreateOptions;
 | 
			
		||||
}
 | 
			
		||||
@ -46,11 +44,10 @@ export interface Assertion {
 | 
			
		||||
 */
 | 
			
		||||
export function transformNewAssertionForServer(newAssertion: PublicKeyCredential): Assertion {
 | 
			
		||||
    const attObj = new Uint8Array(
 | 
			
		||||
        (<AuthenticatorAttestationResponse>newAssertion.response).attestationObject);
 | 
			
		||||
    const clientDataJSON = new Uint8Array(
 | 
			
		||||
        newAssertion.response.clientDataJSON);
 | 
			
		||||
    const rawId = new Uint8Array(
 | 
			
		||||
        newAssertion.rawId);
 | 
			
		||||
        (<AuthenticatorAttestationResponse>newAssertion.response).attestationObject,
 | 
			
		||||
    );
 | 
			
		||||
    const clientDataJSON = new Uint8Array(newAssertion.response.clientDataJSON);
 | 
			
		||||
    const rawId = new Uint8Array(newAssertion.rawId);
 | 
			
		||||
 | 
			
		||||
    const registrationClientExtensions = newAssertion.getClientExtensionResults();
 | 
			
		||||
    return {
 | 
			
		||||
@ -59,26 +56,32 @@ export function transformNewAssertionForServer(newAssertion: PublicKeyCredential
 | 
			
		||||
        type: newAssertion.type,
 | 
			
		||||
        attObj: b64enc(attObj),
 | 
			
		||||
        clientData: b64enc(clientDataJSON),
 | 
			
		||||
        registrationClientExtensions: JSON.stringify(registrationClientExtensions)
 | 
			
		||||
        registrationClientExtensions: JSON.stringify(registrationClientExtensions),
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function u8arr(input: string): Uint8Array {
 | 
			
		||||
    return Uint8Array.from(atob(input.replace(/_/g, "/").replace(/-/g, "+")), c => c.charCodeAt(0));
 | 
			
		||||
    return Uint8Array.from(atob(input.replace(/_/g, "/").replace(/-/g, "+")), (c) =>
 | 
			
		||||
        c.charCodeAt(0),
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function transformCredentialRequestOptions(credentialRequestOptions: PublicKeyCredentialRequestOptions): PublicKeyCredentialRequestOptions {
 | 
			
		||||
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 });
 | 
			
		||||
    });
 | 
			
		||||
    const allowCredentials = (credentialRequestOptions.allowCredentials || []).map(
 | 
			
		||||
        (credentialDescriptor) => {
 | 
			
		||||
            const id = u8arr(credentialDescriptor.id.toString());
 | 
			
		||||
            return Object.assign({}, credentialDescriptor, { id });
 | 
			
		||||
        },
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const transformedCredentialRequestOptions = Object.assign(
 | 
			
		||||
        {},
 | 
			
		||||
        credentialRequestOptions,
 | 
			
		||||
        { challenge, allowCredentials });
 | 
			
		||||
    const transformedCredentialRequestOptions = Object.assign({}, credentialRequestOptions, {
 | 
			
		||||
        challenge,
 | 
			
		||||
        allowCredentials,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return transformedCredentialRequestOptions;
 | 
			
		||||
}
 | 
			
		||||
@ -97,8 +100,8 @@ export interface AuthAssertion {
 | 
			
		||||
 * 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 = <AuthenticatorAssertionResponse> newAssertion.response;
 | 
			
		||||
export function transformAssertionForServer(newAssertion: PublicKeyCredential): AuthAssertion {
 | 
			
		||||
    const response = <AuthenticatorAssertionResponse>newAssertion.response;
 | 
			
		||||
    const authData = new Uint8Array(response.authenticatorData);
 | 
			
		||||
    const clientDataJSON = new Uint8Array(response.clientDataJSON);
 | 
			
		||||
    const rawId = new Uint8Array(newAssertion.rawId);
 | 
			
		||||
@ -112,6 +115,6 @@ export function transformAssertionForServer(newAssertion: PublicKeyCredential):
 | 
			
		||||
        authData: b64RawEnc(authData),
 | 
			
		||||
        clientData: b64RawEnc(clientDataJSON),
 | 
			
		||||
        signature: hexEncode(sig),
 | 
			
		||||
        assertionClientExtensions: JSON.stringify(assertionClientExtensions)
 | 
			
		||||
        assertionClientExtensions: JSON.stringify(assertionClientExtensions),
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -12,36 +12,37 @@ import "../../../elements/EmptyState";
 | 
			
		||||
import { AutosubmitChallenge, AutoSubmitChallengeResponseRequest } from "authentik-api";
 | 
			
		||||
 | 
			
		||||
@customElement("ak-stage-autosubmit")
 | 
			
		||||
export class AutosubmitStage extends BaseStage<AutosubmitChallenge, AutoSubmitChallengeResponseRequest> {
 | 
			
		||||
 | 
			
		||||
export class AutosubmitStage extends BaseStage<
 | 
			
		||||
    AutosubmitChallenge,
 | 
			
		||||
    AutoSubmitChallengeResponseRequest
 | 
			
		||||
> {
 | 
			
		||||
    static get styles(): CSSResult[] {
 | 
			
		||||
        return [PFBase, PFLogin, PFForm, PFFormControl, PFButton, PFTitle, AKGlobal];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    updated(): void {
 | 
			
		||||
        this.shadowRoot?.querySelectorAll("form").forEach((form) => {form.submit();});
 | 
			
		||||
        this.shadowRoot?.querySelectorAll("form").forEach((form) => {
 | 
			
		||||
            form.submit();
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render(): TemplateResult {
 | 
			
		||||
        if (!this.challenge) {
 | 
			
		||||
            return html`<ak-empty-state
 | 
			
		||||
                ?loading="${true}"
 | 
			
		||||
                header=${t`Loading`}>
 | 
			
		||||
            </ak-empty-state>`;
 | 
			
		||||
            return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
 | 
			
		||||
        }
 | 
			
		||||
        return html`<header class="pf-c-login__main-header">
 | 
			
		||||
                <h1 class="pf-c-title pf-m-3xl">
 | 
			
		||||
                    ${this.challenge.flowInfo?.title}
 | 
			
		||||
                </h1>
 | 
			
		||||
                <h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
 | 
			
		||||
            </header>
 | 
			
		||||
            <div class="pf-c-login__main-body">
 | 
			
		||||
                <form class="pf-c-form" action="${this.challenge.url}" method="POST">
 | 
			
		||||
                    ${Object.entries(this.challenge.attrs).map(([ key, value ]) => {
 | 
			
		||||
                        return html`<input type="hidden" name="${key as string}" value="${value as string}">`;
 | 
			
		||||
                    ${Object.entries(this.challenge.attrs).map(([key, value]) => {
 | 
			
		||||
                        return html`<input
 | 
			
		||||
                            type="hidden"
 | 
			
		||||
                            name="${key as string}"
 | 
			
		||||
                            value="${value as string}"
 | 
			
		||||
                        />`;
 | 
			
		||||
                    })}
 | 
			
		||||
                    <ak-empty-state
 | 
			
		||||
                        ?loading="${true}">
 | 
			
		||||
                    </ak-empty-state>
 | 
			
		||||
                    <ak-empty-state ?loading="${true}"> </ak-empty-state>
 | 
			
		||||
                    <div class="pf-c-form__group pf-m-action">
 | 
			
		||||
                        <button type="submit" class="pf-c-button pf-m-primary pf-m-block">
 | 
			
		||||
                            ${t`Continue`}
 | 
			
		||||
@ -50,9 +51,7 @@ export class AutosubmitStage extends BaseStage<AutosubmitChallenge, AutoSubmitCh
 | 
			
		||||
                </form>
 | 
			
		||||
            </div>
 | 
			
		||||
            <footer class="pf-c-login__main-footer">
 | 
			
		||||
                <ul class="pf-c-login__main-footer-links">
 | 
			
		||||
                </ul>
 | 
			
		||||
                <ul class="pf-c-login__main-footer-links"></ul>
 | 
			
		||||
            </footer>`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -7,7 +7,6 @@ export interface StageHost {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class BaseStage<Tin, Tout> extends LitElement {
 | 
			
		||||
 | 
			
		||||
    host!: StageHost;
 | 
			
		||||
 | 
			
		||||
    @property({ attribute: false })
 | 
			
		||||
@ -19,7 +18,7 @@ export class BaseStage<Tin, Tout> extends LitElement {
 | 
			
		||||
            [key: string]: unknown;
 | 
			
		||||
        } = {};
 | 
			
		||||
        const form = new FormData(this.shadowRoot?.querySelector("form") || undefined);
 | 
			
		||||
        form.forEach((value, key) => object[key] = value);
 | 
			
		||||
        form.forEach((value, key) => (object[key] = value));
 | 
			
		||||
        this.host?.submit(object as unknown as Tout);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -28,18 +27,14 @@ export class BaseStage<Tin, Tout> extends LitElement {
 | 
			
		||||
            return html``;
 | 
			
		||||
        }
 | 
			
		||||
        return html`<div class="pf-c-form__alert">
 | 
			
		||||
            ${errors.map(err => {
 | 
			
		||||
            ${errors.map((err) => {
 | 
			
		||||
                return html`<div class="pf-c-alert pf-m-inline pf-m-danger">
 | 
			
		||||
                    <div class="pf-c-alert__icon">
 | 
			
		||||
                        <i class="fas fa-exclamation-circle"></i>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <h4 class="pf-c-alert__title">
 | 
			
		||||
                        ${err.string}
 | 
			
		||||
                    </h4>
 | 
			
		||||
                    <h4 class="pf-c-alert__title">${err.string}</h4>
 | 
			
		||||
                </div>`;
 | 
			
		||||
            })}
 | 
			
		||||
        </div>`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -17,7 +17,6 @@ import { ifDefined } from "lit-html/directives/if-defined";
 | 
			
		||||
 | 
			
		||||
@customElement("ak-stage-captcha")
 | 
			
		||||
export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeResponseRequest> {
 | 
			
		||||
 | 
			
		||||
    static get styles(): CSSResult[] {
 | 
			
		||||
        return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal];
 | 
			
		||||
    }
 | 
			
		||||
@ -38,7 +37,7 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
 | 
			
		||||
                    sitekey: this.challenge.siteKey,
 | 
			
		||||
                    callback: (token) => {
 | 
			
		||||
                        this.host?.submit({
 | 
			
		||||
                            "token": token,
 | 
			
		||||
                            token: token,
 | 
			
		||||
                        });
 | 
			
		||||
                    },
 | 
			
		||||
                    size: "invisible",
 | 
			
		||||
@ -51,24 +50,22 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
 | 
			
		||||
 | 
			
		||||
    render(): TemplateResult {
 | 
			
		||||
        if (!this.challenge) {
 | 
			
		||||
            return html`<ak-empty-state
 | 
			
		||||
                    ?loading="${true}"
 | 
			
		||||
                    header=${t`Loading`}>
 | 
			
		||||
                </ak-empty-state>`;
 | 
			
		||||
            return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
 | 
			
		||||
        }
 | 
			
		||||
        return html`<header class="pf-c-login__main-header">
 | 
			
		||||
                <h1 class="pf-c-title pf-m-3xl">
 | 
			
		||||
                    ${this.challenge.flowInfo?.title}
 | 
			
		||||
                </h1>
 | 
			
		||||
                <h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
 | 
			
		||||
            </header>
 | 
			
		||||
            <div class="pf-c-login__main-body">
 | 
			
		||||
                <form class="pf-c-form">
 | 
			
		||||
                    <ak-form-static
 | 
			
		||||
                        class="pf-c-form__group"
 | 
			
		||||
                        userAvatar="${this.challenge.pendingUserAvatar}"
 | 
			
		||||
                        user=${this.challenge.pendingUser}>
 | 
			
		||||
                        user=${this.challenge.pendingUser}
 | 
			
		||||
                    >
 | 
			
		||||
                        <div slot="link">
 | 
			
		||||
                            <a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}">${t`Not you?`}</a>
 | 
			
		||||
                            <a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
 | 
			
		||||
                                >${t`Not you?`}</a
 | 
			
		||||
                            >
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </ak-form-static>
 | 
			
		||||
                    <div class="ak-loading">
 | 
			
		||||
@ -77,9 +74,7 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
 | 
			
		||||
                </form>
 | 
			
		||||
            </div>
 | 
			
		||||
            <footer class="pf-c-login__main-footer">
 | 
			
		||||
                <ul class="pf-c-login__main-footer-links">
 | 
			
		||||
                </ul>
 | 
			
		||||
                <ul class="pf-c-login__main-footer-links"></ul>
 | 
			
		||||
            </footer>`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -15,48 +15,63 @@ import "../../FormStatic";
 | 
			
		||||
import { ConsentChallenge, ConsentChallengeResponseRequest } from "authentik-api";
 | 
			
		||||
import { ifDefined } from "lit-html/directives/if-defined";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@customElement("ak-stage-consent")
 | 
			
		||||
export class ConsentStage extends BaseStage<ConsentChallenge, ConsentChallengeResponseRequest> {
 | 
			
		||||
 | 
			
		||||
    static get styles(): CSSResult[] {
 | 
			
		||||
        return [PFBase, PFLogin, PFList, PFForm, PFSpacing, PFFormControl, PFTitle, PFButton, AKGlobal];
 | 
			
		||||
        return [
 | 
			
		||||
            PFBase,
 | 
			
		||||
            PFLogin,
 | 
			
		||||
            PFList,
 | 
			
		||||
            PFForm,
 | 
			
		||||
            PFSpacing,
 | 
			
		||||
            PFFormControl,
 | 
			
		||||
            PFTitle,
 | 
			
		||||
            PFButton,
 | 
			
		||||
            AKGlobal,
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render(): TemplateResult {
 | 
			
		||||
        if (!this.challenge) {
 | 
			
		||||
            return html`<ak-empty-state
 | 
			
		||||
                ?loading="${true}"
 | 
			
		||||
                header=${t`Loading`}>
 | 
			
		||||
            </ak-empty-state>`;
 | 
			
		||||
            return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
 | 
			
		||||
        }
 | 
			
		||||
        return html`<header class="pf-c-login__main-header">
 | 
			
		||||
                <h1 class="pf-c-title pf-m-3xl">
 | 
			
		||||
                    ${this.challenge.flowInfo?.title}
 | 
			
		||||
                </h1>
 | 
			
		||||
                <h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
 | 
			
		||||
            </header>
 | 
			
		||||
            <div class="pf-c-login__main-body">
 | 
			
		||||
                <form class="pf-c-form" @submit=${(e: Event) => { this.submitForm(e); }}>
 | 
			
		||||
                <form
 | 
			
		||||
                    class="pf-c-form"
 | 
			
		||||
                    @submit=${(e: Event) => {
 | 
			
		||||
                        this.submitForm(e);
 | 
			
		||||
                    }}
 | 
			
		||||
                >
 | 
			
		||||
                    <ak-form-static
 | 
			
		||||
                        class="pf-c-form__group"
 | 
			
		||||
                        userAvatar="${this.challenge.pendingUserAvatar}"
 | 
			
		||||
                        user=${this.challenge.pendingUser}>
 | 
			
		||||
                        user=${this.challenge.pendingUser}
 | 
			
		||||
                    >
 | 
			
		||||
                        <div slot="link">
 | 
			
		||||
                            <a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}">${t`Not you?`}</a>
 | 
			
		||||
                            <a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
 | 
			
		||||
                                >${t`Not you?`}</a
 | 
			
		||||
                            >
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </ak-form-static>
 | 
			
		||||
                    <div class="pf-c-form__group">
 | 
			
		||||
                        <p id="header-text" class="pf-u-mb-xl">
 | 
			
		||||
                            ${this.challenge.headerText}
 | 
			
		||||
                        </p>
 | 
			
		||||
                        ${this.challenge.permissions.length > 0 ? html`
 | 
			
		||||
                            <p class="pf-u-mb-sm">${t`Application requires following permissions:`}</p>
 | 
			
		||||
                            <ul class="pf-c-list" id="permmissions">
 | 
			
		||||
                                ${this.challenge.permissions.map((permission) => {
 | 
			
		||||
                                    return html`<li data-permission-code="${permission.id}">${permission.name}</li>`;
 | 
			
		||||
                                })}
 | 
			
		||||
                            </ul>
 | 
			
		||||
                        ` : html``}
 | 
			
		||||
                        <p id="header-text" class="pf-u-mb-xl">${this.challenge.headerText}</p>
 | 
			
		||||
                        ${this.challenge.permissions.length > 0
 | 
			
		||||
                            ? html`
 | 
			
		||||
                                  <p class="pf-u-mb-sm">
 | 
			
		||||
                                      ${t`Application requires following permissions:`}
 | 
			
		||||
                                  </p>
 | 
			
		||||
                                  <ul class="pf-c-list" id="permmissions">
 | 
			
		||||
                                      ${this.challenge.permissions.map((permission) => {
 | 
			
		||||
                                          return html`<li data-permission-code="${permission.id}">
 | 
			
		||||
                                              ${permission.name}
 | 
			
		||||
                                          </li>`;
 | 
			
		||||
                                      })}
 | 
			
		||||
                                  </ul>
 | 
			
		||||
                              `
 | 
			
		||||
                            : html``}
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    <div class="pf-c-form__group pf-m-action">
 | 
			
		||||
@ -67,9 +82,7 @@ export class ConsentStage extends BaseStage<ConsentChallenge, ConsentChallengeRe
 | 
			
		||||
                </form>
 | 
			
		||||
            </div>
 | 
			
		||||
            <footer class="pf-c-login__main-footer">
 | 
			
		||||
                <ul class="pf-c-login__main-footer-links">
 | 
			
		||||
                </ul>
 | 
			
		||||
                <ul class="pf-c-login__main-footer-links"></ul>
 | 
			
		||||
            </footer>`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -14,25 +14,24 @@ import { DummyChallenge, DummyChallengeResponseRequest } from "authentik-api";
 | 
			
		||||
 | 
			
		||||
@customElement("ak-stage-dummy")
 | 
			
		||||
export class DummyStage extends BaseStage<DummyChallenge, DummyChallengeResponseRequest> {
 | 
			
		||||
 | 
			
		||||
    static get styles(): CSSResult[] {
 | 
			
		||||
        return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render(): TemplateResult {
 | 
			
		||||
        if (!this.challenge) {
 | 
			
		||||
            return html`<ak-empty-state
 | 
			
		||||
                ?loading="${true}"
 | 
			
		||||
                header=${t`Loading`}>
 | 
			
		||||
            </ak-empty-state>`;
 | 
			
		||||
            return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
 | 
			
		||||
        }
 | 
			
		||||
        return html`<header class="pf-c-login__main-header">
 | 
			
		||||
                <h1 class="pf-c-title pf-m-3xl">
 | 
			
		||||
                    ${this.challenge.flowInfo?.title}
 | 
			
		||||
                </h1>
 | 
			
		||||
                <h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
 | 
			
		||||
            </header>
 | 
			
		||||
            <div class="pf-c-login__main-body">
 | 
			
		||||
                <form class="pf-c-form" @submit=${(e: Event) => { this.submitForm(e); }}>
 | 
			
		||||
                <form
 | 
			
		||||
                    class="pf-c-form"
 | 
			
		||||
                    @submit=${(e: Event) => {
 | 
			
		||||
                        this.submitForm(e);
 | 
			
		||||
                    }}
 | 
			
		||||
                >
 | 
			
		||||
                    <div class="pf-c-form__group pf-m-action">
 | 
			
		||||
                        <button type="submit" class="pf-c-button pf-m-primary pf-m-block">
 | 
			
		||||
                            ${t`Continue`}
 | 
			
		||||
@ -41,9 +40,7 @@ export class DummyStage extends BaseStage<DummyChallenge, DummyChallengeResponse
 | 
			
		||||
                </form>
 | 
			
		||||
            </div>
 | 
			
		||||
            <footer class="pf-c-login__main-footer">
 | 
			
		||||
                <ul class="pf-c-login__main-footer-links">
 | 
			
		||||
                </ul>
 | 
			
		||||
                <ul class="pf-c-login__main-footer-links"></ul>
 | 
			
		||||
            </footer>`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -13,29 +13,26 @@ import { EmailChallenge, EmailChallengeResponseRequest } from "authentik-api";
 | 
			
		||||
 | 
			
		||||
@customElement("ak-stage-email")
 | 
			
		||||
export class EmailStage extends BaseStage<EmailChallenge, EmailChallengeResponseRequest> {
 | 
			
		||||
 | 
			
		||||
    static get styles(): CSSResult[] {
 | 
			
		||||
        return [PFBase, PFLogin, PFForm, PFFormControl, PFButton, PFTitle, AKGlobal];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render(): TemplateResult {
 | 
			
		||||
        if (!this.challenge) {
 | 
			
		||||
            return html`<ak-empty-state
 | 
			
		||||
                ?loading="${true}"
 | 
			
		||||
                header=${t`Loading`}>
 | 
			
		||||
            </ak-empty-state>`;
 | 
			
		||||
            return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
 | 
			
		||||
        }
 | 
			
		||||
        return html`<header class="pf-c-login__main-header">
 | 
			
		||||
                <h1 class="pf-c-title pf-m-3xl">
 | 
			
		||||
                    ${this.challenge.flowInfo?.title}
 | 
			
		||||
                </h1>
 | 
			
		||||
                <h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
 | 
			
		||||
            </header>
 | 
			
		||||
            <div class="pf-c-login__main-body">
 | 
			
		||||
                <form class="pf-c-form" @submit=${(e: Event) => { this.submitForm(e); }}>
 | 
			
		||||
                <form
 | 
			
		||||
                    class="pf-c-form"
 | 
			
		||||
                    @submit=${(e: Event) => {
 | 
			
		||||
                        this.submitForm(e);
 | 
			
		||||
                    }}
 | 
			
		||||
                >
 | 
			
		||||
                    <div class="pf-c-form__group">
 | 
			
		||||
                        <p>
 | 
			
		||||
                            ${t`Check your Emails for a password reset link.`}
 | 
			
		||||
                        </p>
 | 
			
		||||
                        <p>${t`Check your Emails for a password reset link.`}</p>
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    <div class="pf-c-form__group pf-m-action">
 | 
			
		||||
@ -46,9 +43,7 @@ export class EmailStage extends BaseStage<EmailChallenge, EmailChallengeResponse
 | 
			
		||||
                </form>
 | 
			
		||||
            </div>
 | 
			
		||||
            <footer class="pf-c-login__main-footer">
 | 
			
		||||
                <ul class="pf-c-login__main-footer-links">
 | 
			
		||||
                </ul>
 | 
			
		||||
                <ul class="pf-c-login__main-footer-links"></ul>
 | 
			
		||||
            </footer>`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -11,7 +11,12 @@ import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
 | 
			
		||||
import AKGlobal from "../../../authentik.css";
 | 
			
		||||
import "../../../elements/forms/FormElement";
 | 
			
		||||
import "../../../elements/EmptyState";
 | 
			
		||||
import { IdentificationChallenge, IdentificationChallengeResponseRequest, LoginSource, UserFieldsEnum } from "authentik-api";
 | 
			
		||||
import {
 | 
			
		||||
    IdentificationChallenge,
 | 
			
		||||
    IdentificationChallengeResponseRequest,
 | 
			
		||||
    LoginSource,
 | 
			
		||||
    UserFieldsEnum,
 | 
			
		||||
} from "authentik-api";
 | 
			
		||||
 | 
			
		||||
export const PasswordManagerPrefill: {
 | 
			
		||||
    password: string | undefined;
 | 
			
		||||
@ -21,13 +26,27 @@ export const PasswordManagerPrefill: {
 | 
			
		||||
    totp: undefined,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const OR_LIST_FORMATTERS = new Intl.ListFormat("default", { style: "short", type: "disjunction" });
 | 
			
		||||
export const OR_LIST_FORMATTERS = new Intl.ListFormat("default", {
 | 
			
		||||
    style: "short",
 | 
			
		||||
    type: "disjunction",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@customElement("ak-stage-identification")
 | 
			
		||||
export class IdentificationStage extends BaseStage<IdentificationChallenge, IdentificationChallengeResponseRequest> {
 | 
			
		||||
 | 
			
		||||
export class IdentificationStage extends BaseStage<
 | 
			
		||||
    IdentificationChallenge,
 | 
			
		||||
    IdentificationChallengeResponseRequest
 | 
			
		||||
> {
 | 
			
		||||
    static get styles(): CSSResult[] {
 | 
			
		||||
        return [PFBase, PFAlert, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal].concat(
 | 
			
		||||
        return [
 | 
			
		||||
            PFBase,
 | 
			
		||||
            PFAlert,
 | 
			
		||||
            PFLogin,
 | 
			
		||||
            PFForm,
 | 
			
		||||
            PFFormControl,
 | 
			
		||||
            PFTitle,
 | 
			
		||||
            PFButton,
 | 
			
		||||
            AKGlobal,
 | 
			
		||||
        ].concat(
 | 
			
		||||
            css`
 | 
			
		||||
                /* login page's icons */
 | 
			
		||||
                .pf-c-login__main-footer-links-item button {
 | 
			
		||||
@ -41,7 +60,7 @@ export class IdentificationStage extends BaseStage<IdentificationChallenge, Iden
 | 
			
		||||
                    height: 100%;
 | 
			
		||||
                    max-height: var(--pf-c-login__main-footer-links-item-link-svg--Height);
 | 
			
		||||
                }
 | 
			
		||||
            `
 | 
			
		||||
            `,
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -56,12 +75,14 @@ export class IdentificationStage extends BaseStage<IdentificationChallenge, Iden
 | 
			
		||||
        username.setAttribute("autocomplete", "username");
 | 
			
		||||
        username.onkeyup = (ev: Event) => {
 | 
			
		||||
            const el = ev.target as HTMLInputElement;
 | 
			
		||||
            (this.shadowRoot || this).querySelectorAll<HTMLInputElement>("input[name=uidField]").forEach(input => {
 | 
			
		||||
                input.value = el.value;
 | 
			
		||||
                // Because we assume only one input field exists that matches this
 | 
			
		||||
                // call focus so the user can press enter
 | 
			
		||||
                input.focus();
 | 
			
		||||
            });
 | 
			
		||||
            (this.shadowRoot || this)
 | 
			
		||||
                .querySelectorAll<HTMLInputElement>("input[name=uidField]")
 | 
			
		||||
                .forEach((input) => {
 | 
			
		||||
                    input.value = el.value;
 | 
			
		||||
                    // Because we assume only one input field exists that matches this
 | 
			
		||||
                    // call focus so the user can press enter
 | 
			
		||||
                    input.focus();
 | 
			
		||||
                });
 | 
			
		||||
        };
 | 
			
		||||
        wrapperForm.appendChild(username);
 | 
			
		||||
        const password = document.createElement("input");
 | 
			
		||||
@ -79,11 +100,13 @@ export class IdentificationStage extends BaseStage<IdentificationChallenge, Iden
 | 
			
		||||
            PasswordManagerPrefill.password = el.value;
 | 
			
		||||
            // Because password managers fill username, then password,
 | 
			
		||||
            // we need to re-focus the uid_field here too
 | 
			
		||||
            (this.shadowRoot || this).querySelectorAll<HTMLInputElement>("input[name=uidField]").forEach(input => {
 | 
			
		||||
                // Because we assume only one input field exists that matches this
 | 
			
		||||
                // call focus so the user can press enter
 | 
			
		||||
                input.focus();
 | 
			
		||||
            });
 | 
			
		||||
            (this.shadowRoot || this)
 | 
			
		||||
                .querySelectorAll<HTMLInputElement>("input[name=uidField]")
 | 
			
		||||
                .forEach((input) => {
 | 
			
		||||
                    // Because we assume only one input field exists that matches this
 | 
			
		||||
                    // call focus so the user can press enter
 | 
			
		||||
                    input.focus();
 | 
			
		||||
                });
 | 
			
		||||
        };
 | 
			
		||||
        wrapperForm.appendChild(password);
 | 
			
		||||
        const totp = document.createElement("input");
 | 
			
		||||
@ -101,11 +124,13 @@ export class IdentificationStage extends BaseStage<IdentificationChallenge, Iden
 | 
			
		||||
            PasswordManagerPrefill.totp = el.value;
 | 
			
		||||
            // Because totp managers fill username, then password, then optionally,
 | 
			
		||||
            // we need to re-focus the uid_field here too
 | 
			
		||||
            (this.shadowRoot || this).querySelectorAll<HTMLInputElement>("input[name=uidField]").forEach(input => {
 | 
			
		||||
                // Because we assume only one input field exists that matches this
 | 
			
		||||
                // call focus so the user can press enter
 | 
			
		||||
                input.focus();
 | 
			
		||||
            });
 | 
			
		||||
            (this.shadowRoot || this)
 | 
			
		||||
                .querySelectorAll<HTMLInputElement>("input[name=uidField]")
 | 
			
		||||
                .forEach((input) => {
 | 
			
		||||
                    // Because we assume only one input field exists that matches this
 | 
			
		||||
                    // call focus so the user can press enter
 | 
			
		||||
                    input.focus();
 | 
			
		||||
                });
 | 
			
		||||
        };
 | 
			
		||||
        wrapperForm.appendChild(totp);
 | 
			
		||||
    }
 | 
			
		||||
@ -113,16 +138,19 @@ export class IdentificationStage extends BaseStage<IdentificationChallenge, Iden
 | 
			
		||||
    renderSource(source: LoginSource): TemplateResult {
 | 
			
		||||
        let icon = html`<i class="fas fas fa-share-square" title="${source.name}"></i>`;
 | 
			
		||||
        if (source.iconUrl) {
 | 
			
		||||
            icon = html`<img src="${source.iconUrl}" alt="${source.name}">`;
 | 
			
		||||
            icon = html`<img src="${source.iconUrl}" alt="${source.name}" />`;
 | 
			
		||||
        }
 | 
			
		||||
        return html`<li class="pf-c-login__main-footer-links-item">
 | 
			
		||||
                <button type="button" @click=${() => {
 | 
			
		||||
            <button
 | 
			
		||||
                type="button"
 | 
			
		||||
                @click=${() => {
 | 
			
		||||
                    if (!this.host) return;
 | 
			
		||||
                    this.host.challenge = source.challenge;
 | 
			
		||||
                }}>
 | 
			
		||||
                    ${icon}
 | 
			
		||||
                </button>
 | 
			
		||||
            </li>`;
 | 
			
		||||
                }}
 | 
			
		||||
            >
 | 
			
		||||
                ${icon}
 | 
			
		||||
            </button>
 | 
			
		||||
        </li>`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    renderFooter(): TemplateResult {
 | 
			
		||||
@ -130,24 +158,26 @@ export class IdentificationStage extends BaseStage<IdentificationChallenge, Iden
 | 
			
		||||
            return html``;
 | 
			
		||||
        }
 | 
			
		||||
        return html`<div class="pf-c-login__main-footer-band">
 | 
			
		||||
                ${this.challenge.enrollUrl ? html`
 | 
			
		||||
                <p class="pf-c-login__main-footer-band-item">
 | 
			
		||||
                    ${t`Need an account?`}
 | 
			
		||||
                    <a id="enroll" href="${this.challenge.enrollUrl}">${t`Sign up.`}</a>
 | 
			
		||||
                </p>` : html``}
 | 
			
		||||
                ${this.challenge.recoveryUrl ? html`
 | 
			
		||||
                <p class="pf-c-login__main-footer-band-item">
 | 
			
		||||
                    <a id="recovery" href="${this.challenge.recoveryUrl}">${t`Forgot username or password?`}</a>
 | 
			
		||||
                </p>` : html``}
 | 
			
		||||
            </div>`;
 | 
			
		||||
            ${this.challenge.enrollUrl
 | 
			
		||||
                ? html` <p class="pf-c-login__main-footer-band-item">
 | 
			
		||||
                      ${t`Need an account?`}
 | 
			
		||||
                      <a id="enroll" href="${this.challenge.enrollUrl}">${t`Sign up.`}</a>
 | 
			
		||||
                  </p>`
 | 
			
		||||
                : html``}
 | 
			
		||||
            ${this.challenge.recoveryUrl
 | 
			
		||||
                ? html` <p class="pf-c-login__main-footer-band-item">
 | 
			
		||||
                      <a id="recovery" href="${this.challenge.recoveryUrl}"
 | 
			
		||||
                          >${t`Forgot username or password?`}</a
 | 
			
		||||
                      >
 | 
			
		||||
                  </p>`
 | 
			
		||||
                : html``}
 | 
			
		||||
        </div>`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    renderInput(): TemplateResult {
 | 
			
		||||
        let type = "text";
 | 
			
		||||
        if (!this.challenge?.userFields) {
 | 
			
		||||
            return html`<p>
 | 
			
		||||
                ${t`Select one of the sources below to login.`}
 | 
			
		||||
            </p>`;
 | 
			
		||||
            return html`<p>${t`Select one of the sources below to login.`}</p>`;
 | 
			
		||||
        }
 | 
			
		||||
        const fields = (this.challenge?.userFields || []).sort();
 | 
			
		||||
        // Check if the field should be *only* email to set the input type
 | 
			
		||||
@ -159,40 +189,48 @@ export class IdentificationStage extends BaseStage<IdentificationChallenge, Iden
 | 
			
		||||
            [UserFieldsEnum.Email]: t`Email`,
 | 
			
		||||
            [UserFieldsEnum.Upn]: t`UPN`,
 | 
			
		||||
        };
 | 
			
		||||
        const label = OR_LIST_FORMATTERS.format(fields.map(f => uiFields[f]));
 | 
			
		||||
        const label = OR_LIST_FORMATTERS.format(fields.map((f) => uiFields[f]));
 | 
			
		||||
        return html`<ak-form-element
 | 
			
		||||
                label=${label}
 | 
			
		||||
                ?required="${true}"
 | 
			
		||||
                class="pf-c-form__group"
 | 
			
		||||
                .errors=${(this.challenge.responseErrors || {})["uid_field"]}>
 | 
			
		||||
                .errors=${(this.challenge.responseErrors || {})["uid_field"]}
 | 
			
		||||
            >
 | 
			
		||||
                <!-- @ts-ignore -->
 | 
			
		||||
                <input type=${type}
 | 
			
		||||
                <input
 | 
			
		||||
                    type=${type}
 | 
			
		||||
                    name="uidField"
 | 
			
		||||
                    placeholder=${label}
 | 
			
		||||
                    autofocus=""
 | 
			
		||||
                    autocomplete="username"
 | 
			
		||||
                    class="pf-c-form-control"
 | 
			
		||||
                    required>
 | 
			
		||||
                    required
 | 
			
		||||
                />
 | 
			
		||||
            </ak-form-element>
 | 
			
		||||
            ${this.challenge.passwordFields ? html`
 | 
			
		||||
                <ak-form-element
 | 
			
		||||
                    label="${t`Password`}"
 | 
			
		||||
                    ?required="${true}"
 | 
			
		||||
                    class="pf-c-form__group"
 | 
			
		||||
                    .errors=${(this.challenge.responseErrors || {})["password"]}>
 | 
			
		||||
                    <input type="password"
 | 
			
		||||
                        name="password"
 | 
			
		||||
                        placeholder="${t`Password`}"
 | 
			
		||||
                        autofocus=""
 | 
			
		||||
                        autocomplete="current-password"
 | 
			
		||||
                        class="pf-c-form-control"
 | 
			
		||||
                        required
 | 
			
		||||
                        value=${PasswordManagerPrefill.password || ""}>
 | 
			
		||||
                </ak-form-element>
 | 
			
		||||
            `: html``}
 | 
			
		||||
            ${"non_field_errors" in (this.challenge?.responseErrors || {}) ?
 | 
			
		||||
                this.renderNonFieldErrors(this.challenge?.responseErrors?.non_field_errors || []) :
 | 
			
		||||
                html``}
 | 
			
		||||
            ${this.challenge.passwordFields
 | 
			
		||||
                ? html`
 | 
			
		||||
                      <ak-form-element
 | 
			
		||||
                          label="${t`Password`}"
 | 
			
		||||
                          ?required="${true}"
 | 
			
		||||
                          class="pf-c-form__group"
 | 
			
		||||
                          .errors=${(this.challenge.responseErrors || {})["password"]}
 | 
			
		||||
                      >
 | 
			
		||||
                          <input
 | 
			
		||||
                              type="password"
 | 
			
		||||
                              name="password"
 | 
			
		||||
                              placeholder="${t`Password`}"
 | 
			
		||||
                              autofocus=""
 | 
			
		||||
                              autocomplete="current-password"
 | 
			
		||||
                              class="pf-c-form-control"
 | 
			
		||||
                              required
 | 
			
		||||
                              value=${PasswordManagerPrefill.password || ""}
 | 
			
		||||
                          />
 | 
			
		||||
                      </ak-form-element>
 | 
			
		||||
                  `
 | 
			
		||||
                : html``}
 | 
			
		||||
            ${"non_field_errors" in (this.challenge?.responseErrors || {})
 | 
			
		||||
                ? this.renderNonFieldErrors(this.challenge?.responseErrors?.non_field_errors || [])
 | 
			
		||||
                : html``}
 | 
			
		||||
            <div class="pf-c-form__group pf-m-action">
 | 
			
		||||
                <button type="submit" class="pf-c-button pf-m-primary pf-m-block">
 | 
			
		||||
                    ${this.challenge.primaryAction}
 | 
			
		||||
@ -202,23 +240,21 @@ export class IdentificationStage extends BaseStage<IdentificationChallenge, Iden
 | 
			
		||||
 | 
			
		||||
    render(): TemplateResult {
 | 
			
		||||
        if (!this.challenge) {
 | 
			
		||||
            return html`<ak-empty-state
 | 
			
		||||
                ?loading="${true}"
 | 
			
		||||
                header=${t`Loading`}>
 | 
			
		||||
            </ak-empty-state>`;
 | 
			
		||||
            return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
 | 
			
		||||
        }
 | 
			
		||||
        return html`<header class="pf-c-login__main-header">
 | 
			
		||||
                <h1 class="pf-c-title pf-m-3xl">
 | 
			
		||||
                    ${this.challenge.flowInfo?.title}
 | 
			
		||||
                </h1>
 | 
			
		||||
                <h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
 | 
			
		||||
            </header>
 | 
			
		||||
            <div class="pf-c-login__main-body">
 | 
			
		||||
                <form class="pf-c-form" @submit=${(e: Event) => {this.submitForm(e);}}>
 | 
			
		||||
                    ${this.challenge.applicationPre ?
 | 
			
		||||
                        html`<p>
 | 
			
		||||
                            ${t`Login to continue to ${this.challenge.applicationPre}.`}
 | 
			
		||||
                        </p>`:
 | 
			
		||||
                        html``}
 | 
			
		||||
                <form
 | 
			
		||||
                    class="pf-c-form"
 | 
			
		||||
                    @submit=${(e: Event) => {
 | 
			
		||||
                        this.submitForm(e);
 | 
			
		||||
                    }}
 | 
			
		||||
                >
 | 
			
		||||
                    ${this.challenge.applicationPre
 | 
			
		||||
                        ? html`<p>${t`Login to continue to ${this.challenge.applicationPre}.`}</p>`
 | 
			
		||||
                        : html``}
 | 
			
		||||
                    ${this.renderInput()}
 | 
			
		||||
                </form>
 | 
			
		||||
            </div>
 | 
			
		||||
@ -231,5 +267,4 @@ export class IdentificationStage extends BaseStage<IdentificationChallenge, Iden
 | 
			
		||||
                ${this.renderFooter()}
 | 
			
		||||
            </footer>`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -17,52 +17,62 @@ import { ifDefined } from "lit-html/directives/if-defined";
 | 
			
		||||
 | 
			
		||||
@customElement("ak-stage-password")
 | 
			
		||||
export class PasswordStage extends BaseStage<PasswordChallenge, PasswordChallengeResponseRequest> {
 | 
			
		||||
 | 
			
		||||
    static get styles(): CSSResult[] {
 | 
			
		||||
        return [PFBase, PFLogin, PFForm, PFFormControl, PFButton, PFTitle, AKGlobal];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render(): TemplateResult {
 | 
			
		||||
        if (!this.challenge) {
 | 
			
		||||
            return html`<ak-empty-state
 | 
			
		||||
                ?loading="${true}"
 | 
			
		||||
                header=${t`Loading`}>
 | 
			
		||||
            </ak-empty-state>`;
 | 
			
		||||
            return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
 | 
			
		||||
        }
 | 
			
		||||
        return html`<header class="pf-c-login__main-header">
 | 
			
		||||
                <h1 class="pf-c-title pf-m-3xl">
 | 
			
		||||
                    ${this.challenge.flowInfo?.title}
 | 
			
		||||
                </h1>
 | 
			
		||||
                <h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
 | 
			
		||||
            </header>
 | 
			
		||||
            <div class="pf-c-login__main-body">
 | 
			
		||||
                <form class="pf-c-form" @submit=${(e: Event) => {this.submitForm(e);}}>
 | 
			
		||||
                <form
 | 
			
		||||
                    class="pf-c-form"
 | 
			
		||||
                    @submit=${(e: Event) => {
 | 
			
		||||
                        this.submitForm(e);
 | 
			
		||||
                    }}
 | 
			
		||||
                >
 | 
			
		||||
                    <ak-form-static
 | 
			
		||||
                        class="pf-c-form__group"
 | 
			
		||||
                        userAvatar="${this.challenge.pendingUserAvatar}"
 | 
			
		||||
                        user=${this.challenge.pendingUser}>
 | 
			
		||||
                        user=${this.challenge.pendingUser}
 | 
			
		||||
                    >
 | 
			
		||||
                        <div slot="link">
 | 
			
		||||
                            <a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}">${t`Not you?`}</a>
 | 
			
		||||
                            <a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
 | 
			
		||||
                                >${t`Not you?`}</a
 | 
			
		||||
                            >
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </ak-form-static>
 | 
			
		||||
                    <input name="username" autocomplete="username" type="hidden" value="${this.challenge.pendingUser}">
 | 
			
		||||
                    <input
 | 
			
		||||
                        name="username"
 | 
			
		||||
                        autocomplete="username"
 | 
			
		||||
                        type="hidden"
 | 
			
		||||
                        value="${this.challenge.pendingUser}"
 | 
			
		||||
                    />
 | 
			
		||||
                    <ak-form-element
 | 
			
		||||
                        label="${t`Password`}"
 | 
			
		||||
                        ?required="${true}"
 | 
			
		||||
                        class="pf-c-form__group"
 | 
			
		||||
                        .errors=${(this.challenge?.responseErrors || {})["password"]}>
 | 
			
		||||
                        <input type="password"
 | 
			
		||||
                        .errors=${(this.challenge?.responseErrors || {})["password"]}
 | 
			
		||||
                    >
 | 
			
		||||
                        <input
 | 
			
		||||
                            type="password"
 | 
			
		||||
                            name="password"
 | 
			
		||||
                            placeholder="${t`Please enter your password`}"
 | 
			
		||||
                            autofocus=""
 | 
			
		||||
                            autocomplete="current-password"
 | 
			
		||||
                            class="pf-c-form-control"
 | 
			
		||||
                            required
 | 
			
		||||
                            value=${PasswordManagerPrefill.password || ""}>
 | 
			
		||||
                            value=${PasswordManagerPrefill.password || ""}
 | 
			
		||||
                        />
 | 
			
		||||
                    </ak-form-element>
 | 
			
		||||
 | 
			
		||||
                    ${this.challenge.recoveryUrl ?
 | 
			
		||||
                        html`<a href="${this.challenge.recoveryUrl}">
 | 
			
		||||
                        ${t`Forgot password?`}</a>` : ""}
 | 
			
		||||
                    ${this.challenge.recoveryUrl
 | 
			
		||||
                        ? html`<a href="${this.challenge.recoveryUrl}"> ${t`Forgot password?`}</a>`
 | 
			
		||||
                        : ""}
 | 
			
		||||
 | 
			
		||||
                    <div class="pf-c-form__group pf-m-action">
 | 
			
		||||
                        <button type="submit" class="pf-c-button pf-m-primary pf-m-block">
 | 
			
		||||
@ -72,9 +82,7 @@ export class PasswordStage extends BaseStage<PasswordChallenge, PasswordChalleng
 | 
			
		||||
                </form>
 | 
			
		||||
            </div>
 | 
			
		||||
            <footer class="pf-c-login__main-footer">
 | 
			
		||||
                <ul class="pf-c-login__main-footer-links">
 | 
			
		||||
                </ul>
 | 
			
		||||
                <ul class="pf-c-login__main-footer-links"></ul>
 | 
			
		||||
            </footer>`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -15,10 +15,8 @@ import "../../../elements/EmptyState";
 | 
			
		||||
import "../../../elements/Divider";
 | 
			
		||||
import { PromptChallenge, PromptChallengeResponseRequest, StagePrompt } from "authentik-api";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@customElement("ak-stage-prompt")
 | 
			
		||||
export class PromptStage extends BaseStage<PromptChallenge, PromptChallengeResponseRequest> {
 | 
			
		||||
 | 
			
		||||
    static get styles(): CSSResult[] {
 | 
			
		||||
        return [PFBase, PFLogin, PFAlert, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal];
 | 
			
		||||
    }
 | 
			
		||||
@ -104,34 +102,41 @@ export class PromptStage extends BaseStage<PromptChallenge, PromptChallengeRespo
 | 
			
		||||
 | 
			
		||||
    render(): TemplateResult {
 | 
			
		||||
        if (!this.challenge) {
 | 
			
		||||
            return html`<ak-empty-state
 | 
			
		||||
                ?loading="${true}"
 | 
			
		||||
                header=${t`Loading`}>
 | 
			
		||||
            </ak-empty-state>`;
 | 
			
		||||
            return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
 | 
			
		||||
        }
 | 
			
		||||
        return html`<header class="pf-c-login__main-header">
 | 
			
		||||
                <h1 class="pf-c-title pf-m-3xl">
 | 
			
		||||
                    ${this.challenge.flowInfo?.title}
 | 
			
		||||
                </h1>
 | 
			
		||||
                <h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
 | 
			
		||||
            </header>
 | 
			
		||||
            <div class="pf-c-login__main-body">
 | 
			
		||||
                <form class="pf-c-form" @submit=${(e: Event) => {this.submitForm(e);}}>
 | 
			
		||||
                <form
 | 
			
		||||
                    class="pf-c-form"
 | 
			
		||||
                    @submit=${(e: Event) => {
 | 
			
		||||
                        this.submitForm(e);
 | 
			
		||||
                    }}
 | 
			
		||||
                >
 | 
			
		||||
                    ${this.challenge.fields.map((prompt) => {
 | 
			
		||||
                        // Special types that aren't rendered in a wrapper
 | 
			
		||||
                        if (prompt.type === "static" || prompt.type === "hidden" || prompt.type === "separator") {
 | 
			
		||||
                        if (
 | 
			
		||||
                            prompt.type === "static" ||
 | 
			
		||||
                            prompt.type === "hidden" ||
 | 
			
		||||
                            prompt.type === "separator"
 | 
			
		||||
                        ) {
 | 
			
		||||
                            return unsafeHTML(this.renderPromptInner(prompt));
 | 
			
		||||
                        }
 | 
			
		||||
                        return html`<ak-form-element
 | 
			
		||||
                            label="${prompt.label}"
 | 
			
		||||
                            ?required="${prompt.required}"
 | 
			
		||||
                            class="pf-c-form__group"
 | 
			
		||||
                            .errors=${(this.challenge?.responseErrors || {})[prompt.fieldKey]}>
 | 
			
		||||
                            .errors=${(this.challenge?.responseErrors || {})[prompt.fieldKey]}
 | 
			
		||||
                        >
 | 
			
		||||
                            ${unsafeHTML(this.renderPromptInner(prompt))}
 | 
			
		||||
                        </ak-form-element>`;
 | 
			
		||||
                    })}
 | 
			
		||||
                    ${"non_field_errors" in (this.challenge?.responseErrors || {}) ?
 | 
			
		||||
                        this.renderNonFieldErrors(this.challenge?.responseErrors?.non_field_errors || []):
 | 
			
		||||
                        html``}
 | 
			
		||||
                    ${"non_field_errors" in (this.challenge?.responseErrors || {})
 | 
			
		||||
                        ? this.renderNonFieldErrors(
 | 
			
		||||
                              this.challenge?.responseErrors?.non_field_errors || [],
 | 
			
		||||
                          )
 | 
			
		||||
                        : html``}
 | 
			
		||||
                    <div class="pf-c-form__group pf-m-action">
 | 
			
		||||
                        <button type="submit" class="pf-c-button pf-m-primary pf-m-block">
 | 
			
		||||
                            ${t`Continue`}
 | 
			
		||||
@ -140,9 +145,7 @@ export class PromptStage extends BaseStage<PromptChallenge, PromptChallengeRespo
 | 
			
		||||
                </form>
 | 
			
		||||
            </div>
 | 
			
		||||
            <footer class="pf-c-login__main-footer">
 | 
			
		||||
                <ul class="pf-c-login__main-footer-links">
 | 
			
		||||
                </ul>
 | 
			
		||||
                <ul class="pf-c-login__main-footer-links"></ul>
 | 
			
		||||
            </footer>`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user