web: improve compatibility with password managers

This commit is contained in:
Jens Langhammer 2021-03-11 21:57:20 +01:00
parent d99451b45c
commit 899cf392f4
4 changed files with 83 additions and 1 deletions

View file

@ -5,6 +5,12 @@ html {
--pf-c-nav__link--PaddingLeft: 0.5rem; --pf-c-nav__link--PaddingLeft: 0.5rem;
} }
html > input {
position: absolute;
top: -2000px;
left: -2000px;
}
.pf-c-page__header { .pf-c-page__header {
z-index: 0; z-index: 0;
} }

View file

@ -5,6 +5,7 @@ import { BaseStage } from "../base";
import { AuthenticatorValidateStage, AuthenticatorValidateStageChallenge, DeviceChallenge } from "./AuthenticatorValidateStage"; import { AuthenticatorValidateStage, AuthenticatorValidateStageChallenge, DeviceChallenge } from "./AuthenticatorValidateStage";
import "../form"; import "../form";
import "../../../elements/utils/LoadingState"; import "../../../elements/utils/LoadingState";
import { PasswordManagerPrefill } from "../identification/IdentificationStage";
@customElement("ak-stage-authenticator-validate-code") @customElement("ak-stage-authenticator-validate-code")
export class AuthenticatorValidateStageWebCode extends BaseStage { export class AuthenticatorValidateStageWebCode extends BaseStage {
@ -53,6 +54,7 @@ export class AuthenticatorValidateStageWebCode extends BaseStage {
autofocus="" autofocus=""
autocomplete="one-time-code" autocomplete="one-time-code"
class="pf-c-form-control" class="pf-c-form-control"
value="${PasswordManagerPrefill.totp || ""}"
required=""> required="">
</ak-form-element> </ak-form-element>

View file

@ -6,6 +6,14 @@ import "../form";
import "../../../elements/utils/LoadingState"; import "../../../elements/utils/LoadingState";
import { Challenge } from "../../../api/Flows"; import { Challenge } from "../../../api/Flows";
export const PasswordManagerPrefill: {
password: string | undefined;
totp: string | undefined;
} = {
password: undefined,
totp: undefined,
};
export interface IdentificationChallenge extends Challenge { export interface IdentificationChallenge extends Challenge {
input_type: string; 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<HTMLInputElement>("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<HTMLInputElement>("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<HTMLInputElement>("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 { renderSource(source: UILoginButton): TemplateResult {
let icon = html`<i class="pf-icon pf-icon-arrow" title="${source.name}"></i>`; let icon = html`<i class="pf-icon pf-icon-arrow" title="${source.name}"></i>`;
if (source.icon_url) { if (source.icon_url) {

View file

@ -5,6 +5,7 @@ import { COMMON_STYLES } from "../../../common/styles";
import { BaseStage } from "../base"; import { BaseStage } from "../base";
import "../form"; import "../form";
import "../../../elements/utils/LoadingState"; import "../../../elements/utils/LoadingState";
import { PasswordManagerPrefill } from "../identification/IdentificationStage";
export interface PasswordChallenge extends WithUserInfoChallenge { export interface PasswordChallenge extends WithUserInfoChallenge {
recovery_url?: string; recovery_url?: string;
@ -43,6 +44,7 @@ export class PasswordStage extends BaseStage {
</div> </div>
</div> </div>
<input name="username" autocomplete="username" type="hidden" value="${this.challenge.pending_user}">
<ak-form-element <ak-form-element
label="${gettext("Password")}" label="${gettext("Password")}"
?required="${true}" ?required="${true}"
@ -54,7 +56,8 @@ export class PasswordStage extends BaseStage {
autofocus="" autofocus=""
autocomplete="current-password" autocomplete="current-password"
class="pf-c-form-control" class="pf-c-form-control"
required=""> required=""
value=${PasswordManagerPrefill.password || ""}>
</ak-form-element> </ak-form-element>
${this.challenge.recovery_url ? ${this.challenge.recovery_url ?