import { gettext } from "django"; 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"; import PFTitle from "@patternfly/patternfly/components/Title/title.css"; import PFBackgroundImage from "@patternfly/patternfly/components/BackgroundImage/background-image.css"; import PFList from "@patternfly/patternfly/components/List/list.css"; import AKGlobal from "../authentik.css"; import { unsafeHTML } from "lit-html/directives/unsafe-html"; import "./stages/authenticator_static/AuthenticatorStaticStage"; import "./stages/authenticator_totp/AuthenticatorTOTPStage"; import "./stages/authenticator_validate/AuthenticatorValidateStage"; import "./stages/authenticator_webauthn/WebAuthnAuthenticatorRegisterStage"; import "./stages/autosubmit/AutosubmitStage"; import "./stages/captcha/CaptchaStage"; import "./stages/consent/ConsentStage"; import "./stages/email/EmailStage"; import "./stages/identification/IdentificationStage"; import "./stages/password/PasswordStage"; import "./stages/prompt/PromptStage"; import "./access_denied/FlowAccessDenied"; import { ShellChallenge, RedirectChallenge } from "../api/Flows"; import { IdentificationChallenge } from "./stages/identification/IdentificationStage"; import { PasswordChallenge } from "./stages/password/PasswordStage"; import { ConsentChallenge } from "./stages/consent/ConsentStage"; import { EmailChallenge } from "./stages/email/EmailStage"; import { AutosubmitChallenge } from "./stages/autosubmit/AutosubmitStage"; import { PromptChallenge } from "./stages/prompt/PromptStage"; import { AuthenticatorTOTPChallenge } from "./stages/authenticator_totp/AuthenticatorTOTPStage"; import { AuthenticatorStaticChallenge } from "./stages/authenticator_static/AuthenticatorStaticStage"; import { AuthenticatorValidateStageChallenge } from "./stages/authenticator_validate/AuthenticatorValidateStage"; import { WebAuthnAuthenticatorRegisterChallenge } from "./stages/authenticator_webauthn/WebAuthnAuthenticatorRegisterStage"; import { CaptchaChallenge } from "./stages/captcha/CaptchaStage"; import { StageHost } from "./stages/base"; import { Challenge, ChallengeTypeEnum, Config, FlowsApi, RootApi } from "authentik-api"; import { DEFAULT_CONFIG } from "../api/Config"; import { ifDefined } from "lit-html/directives/if-defined"; import { until } from "lit-html/directives/until"; import { TITLE_SUFFIX } from "../elements/router/RouterOutlet"; import { AccessDeniedChallenge } from "./access_denied/FlowAccessDenied"; import { getQueryVariables } from "./utils"; import { SpinnerSize } from "../elements/Spinner"; export const NEXT_ARG = "next"; @customElement("ak-flow-executor") export class FlowExecutor extends LitElement implements StageHost { flowSlug: string; @property({attribute: false}) challenge?: Challenge; @property({type: Boolean}) loading = false; @property({ attribute: false }) config?: Config; static get styles(): CSSResult[] { return [PFBase, PFLogin, PFTitle, PFList, PFBackgroundImage, AKGlobal].concat(css` .ak-loading { display: flex; height: 100%; width: 100%; justify-content: center; align-items: center; position: absolute; background-color: var(--pf-global--BackgroundColor--dark-transparent-100); z-index: 1; } .ak-hidden { display: none; } :host { position: relative; } .ak-exception { font-family: monospace; overflow-x: scroll; } `); } constructor() { super(); this.addEventListener("ak-flow-submit", () => { this.submit(); }); this.flowSlug = window.location.pathname.split("/")[3]; } setBackground(url: string): void { this.shadowRoot?.querySelectorAll(".pf-c-background-image").forEach((bg) => { bg.style.setProperty("--ak-flow-background", `url('${url}')`); }); } private postUpdate(): void { if (this.challenge?.title) { document.title = `${this.challenge.title} - ${TITLE_SUFFIX}`; } else { document.title = TITLE_SUFFIX; } } submit(formData?: T): Promise { this.loading = true; return new FlowsApi(DEFAULT_CONFIG).flowsExecutorSolveRaw({ flowSlug: this.flowSlug, data: formData || {}, }).then((challengeRaw) => { return challengeRaw.raw.json(); }).then((data) => { this.challenge = data; this.postUpdate(); }).catch((e) => { this.errorMessage(e); }).finally(() => { this.loading = false; }); } firstUpdated(): void { new RootApi(DEFAULT_CONFIG).rootConfigList().then((config) => { this.config = config; }); this.loading = true; new FlowsApi(DEFAULT_CONFIG).flowsExecutorGetRaw({ flowSlug: this.flowSlug }).then((challengeRaw) => { return challengeRaw.raw.json(); }).then((challenge) => { this.challenge = challenge as Challenge; // Only set background on first update, flow won't change throughout execution if (this.challenge?.background) { this.setBackground(this.challenge.background); } this.postUpdate(); }).catch((e) => { // Catch JSON or Update errors this.errorMessage(e); }).finally(() => { this.loading = false; }); } errorMessage(error: string): void { this.challenge = { type: ChallengeTypeEnum.Shell, body: ` ` }; } renderLoading(): TemplateResult { return html`
`; } private redirect(challenge: RedirectChallenge): void { // Check if there is a ?next arg and save it // this is used for deep linking, if a user tries to access an application, // but needs to authenticate first const queryVars = getQueryVariables(); localStorage.clear(); if (NEXT_ARG in queryVars) { const next = queryVars[NEXT_ARG]; console.debug("authentik/flows: redirecting to saved url", next); window.location.assign(next); return; } console.debug("authentik/flows: redirecting to url from server", challenge.to); window.location.assign(challenge.to); } renderChallenge(): TemplateResult { if (!this.challenge) { return html``; } switch (this.challenge.type) { case ChallengeTypeEnum.Redirect: this.redirect(this.challenge as RedirectChallenge); return html` `; case ChallengeTypeEnum.Shell: return html`${unsafeHTML((this.challenge as ShellChallenge).body)}`; case ChallengeTypeEnum.Native: switch (this.challenge.component) { case "ak-stage-access-denied": return html``; case "ak-stage-identification": return html``; case "ak-stage-password": return html``; case "ak-stage-captcha": return html``; case "ak-stage-consent": return html``; case "ak-stage-email": return html``; case "ak-stage-autosubmit": return html``; case "ak-stage-prompt": return html``; case "ak-stage-authenticator-totp": return html``; case "ak-stage-authenticator-static": return html``; case "ak-stage-authenticator-webauthn": return html``; case "ak-stage-authenticator-validate": return html``; default: break; } break; default: console.debug(`authentik/flows: unexpected data type ${this.challenge.type}`); break; } return html``; } renderChallengeWrapper(): TemplateResult { if (!this.challenge) { return html` `; } return html` ${this.loading ? this.renderLoading() : html``} ${this.renderChallenge()} `; } render(): TemplateResult { return html`
`; } }