diff --git a/web/src/authentik.css b/web/src/authentik.css index df941a84a..c391694a9 100644 --- a/web/src/authentik.css +++ b/web/src/authentik.css @@ -5,6 +5,12 @@ html { --pf-c-nav__link--PaddingLeft: 0.5rem; } +html > input { + position: absolute; + top: -2000px; + left: -2000px; +} + .pf-c-page__header { z-index: 0; } diff --git a/web/src/flows/stages/authenticator_validate/AuthenticatorValidateStageCode.ts b/web/src/flows/stages/authenticator_validate/AuthenticatorValidateStageCode.ts index 928d4fa36..4e8441629 100644 --- a/web/src/flows/stages/authenticator_validate/AuthenticatorValidateStageCode.ts +++ b/web/src/flows/stages/authenticator_validate/AuthenticatorValidateStageCode.ts @@ -5,6 +5,7 @@ import { BaseStage } from "../base"; import { AuthenticatorValidateStage, AuthenticatorValidateStageChallenge, DeviceChallenge } from "./AuthenticatorValidateStage"; import "../form"; import "../../../elements/utils/LoadingState"; +import { PasswordManagerPrefill } from "../identification/IdentificationStage"; @customElement("ak-stage-authenticator-validate-code") export class AuthenticatorValidateStageWebCode extends BaseStage { @@ -53,6 +54,7 @@ export class AuthenticatorValidateStageWebCode extends BaseStage { autofocus="" autocomplete="one-time-code" class="pf-c-form-control" + value="${PasswordManagerPrefill.totp || ""}" required=""> diff --git a/web/src/flows/stages/identification/IdentificationStage.ts b/web/src/flows/stages/identification/IdentificationStage.ts index 3db34d604..f1d632816 100644 --- a/web/src/flows/stages/identification/IdentificationStage.ts +++ b/web/src/flows/stages/identification/IdentificationStage.ts @@ -6,6 +6,14 @@ import "../form"; import "../../../elements/utils/LoadingState"; import { Challenge } from "../../../api/Flows"; +export const PasswordManagerPrefill: { + password: string | undefined; + totp: string | undefined; +} = { + password: undefined, + totp: undefined, +}; + export interface IdentificationChallenge extends Challenge { input_type: string; @@ -41,6 +49,69 @@ export class IdentificationStage extends BaseStage { ); } + firstUpdated(): void { + // This is a workaround for the fact that we're in a shadow dom + // adapted from https://github.com/home-assistant/frontend/issues/3133 + const username = document.createElement("input"); + username.setAttribute("type", "text"); + username.setAttribute("name", "username"); // username as name for high compatibility + username.setAttribute("autocomplete", "username"); + username.onkeyup = (ev: Event) => { + const el = ev.target as HTMLInputElement; + (this.shadowRoot || this).querySelectorAll("input[name=uid_field]").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(); + }); + }; + document.documentElement.appendChild(username); + const password = document.createElement("input"); + password.setAttribute("type", "password"); + password.setAttribute("name", "password"); + password.setAttribute("autocomplete", "current-password"); + password.onkeyup = (ev: KeyboardEvent) => { + if (ev.key == "Enter") { + this.submitForm(ev); + } + const el = ev.target as HTMLInputElement; + // Because the password field is not actually on this page, + // and we want to 'prefill' the password for the user, + // save it globally + 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("input[name=uid_field]").forEach(input => { + // Because we assume only one input field exists that matches this + // call focus so the user can press enter + input.focus(); + }); + }; + document.documentElement.appendChild(password); + const totp = document.createElement("input"); + totp.setAttribute("type", "text"); + totp.setAttribute("name", "code"); + totp.setAttribute("autocomplete", "one-time-code"); + totp.onkeyup = (ev: KeyboardEvent) => { + if (ev.key == "Enter") { + this.submitForm(ev); + } + const el = ev.target as HTMLInputElement; + // Because the totp field is not actually on this page, + // and we want to 'prefill' the totp for the user, + // save it globally + 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("input[name=uid_field]").forEach(input => { + // Because we assume only one input field exists that matches this + // call focus so the user can press enter + input.focus(); + }); + }; + document.documentElement.appendChild(totp); + } + renderSource(source: UILoginButton): TemplateResult { let icon = html``; if (source.icon_url) { diff --git a/web/src/flows/stages/password/PasswordStage.ts b/web/src/flows/stages/password/PasswordStage.ts index b7e09ba34..1b2e6dc85 100644 --- a/web/src/flows/stages/password/PasswordStage.ts +++ b/web/src/flows/stages/password/PasswordStage.ts @@ -5,6 +5,7 @@ import { COMMON_STYLES } from "../../../common/styles"; import { BaseStage } from "../base"; import "../form"; import "../../../elements/utils/LoadingState"; +import { PasswordManagerPrefill } from "../identification/IdentificationStage"; export interface PasswordChallenge extends WithUserInfoChallenge { recovery_url?: string; @@ -43,6 +44,7 @@ export class PasswordStage extends BaseStage { + + required="" + value=${PasswordManagerPrefill.password || ""}> ${this.challenge.recovery_url ?