Account: disallow leading/trailing whitespace in passwords (#35133)

This commit is contained in:
Szilard Szaloki
2026-04-02 00:28:24 +02:00
committed by GitHub
parent e0365ec5d6
commit dec3cd74aa
13 changed files with 117 additions and 94 deletions
@@ -47,12 +47,10 @@ build_webui("build") {
css_files = [
"brave_account_common.css",
"brave_account_dialog.css",
"brave_account_email_input.css",
"brave_account_entry_dialog.css",
"brave_account_forgot_password_dialog.css",
"brave_account_otp_input.css",
"brave_account_password_icons.css",
"brave_account_password_input.css",
"brave_account_password_strength_meter.css",
"brave_account_sign_in_dialog.css",
]
@@ -7,6 +7,21 @@
* #type=style-lit
* #css_wrapper_metadata_end */
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.label-container {
display: flex;
justify-content: space-between;
width: 100%;
}
.label {
color: var(--leo-color-text-primary);
font: var(--leo-font-small-semibold);
@@ -43,9 +58,22 @@ leo-input:focus:has(.error) {
}
.dropdown-content {
--leo-icon-size: var(--leo-icon-xs);
align-self: end;
animation: fadeIn 300ms ease-in-out;
display: flex;
font: var(--leo-font-small-regular);
gap: var(--leo-spacing-s);
min-height: 0;
leo-icon {
/* Slight vertical tweak to align icon with first text line.
align-items: center on .dropdown-content works for single-line text,
but misaligns when wrapping. Offset is based on the difference between
the line-height and icon size. */
margin-top: calc((1lh - var(--leo-icon-size)) / 2);
}
leo-icon[name='check-circle-filled'] {
--leo-icon-color: var(--leo-color-systemfeedback-success-icon);
@@ -42,6 +42,7 @@ export function getHtml(this: BraveAccountCreateDialogElement) {
placeholder="$i18n{BRAVE_ACCOUNT_PASSWORD_INPUT_PLACEHOLDER}"
@password-input=${(e: CustomEvent<PasswordInputEventDetail>) => {
this.password = e.detail.password
this.isPasswordValid = e.detail.isValid
}}
>
</brave-account-password-input>
@@ -59,6 +60,7 @@ export function getHtml(this: BraveAccountCreateDialogElement) {
<leo-button
slot="buttons"
?isDisabled=${!this.isEmailValid
|| !this.isPasswordValid
|| !this.isPasswordStrongEnough
|| this.passwordConfirmation !== this.password}
@click=${this.onCreateAccountButtonClicked}
@@ -34,6 +34,7 @@ export class BraveAccountCreateDialogElement extends CrLitElement {
isCapsLockOn: { type: Boolean },
isEmailValid: { type: Boolean },
isPasswordStrongEnough: { type: Boolean },
isPasswordValid: { type: Boolean },
password: { type: String },
passwordConfirmation: { type: String },
}
@@ -94,6 +95,7 @@ export class BraveAccountCreateDialogElement extends CrLitElement {
protected accessor isCapsLockOn: boolean = false
protected accessor isEmailValid: boolean = false
protected accessor isPasswordStrongEnough: boolean = false
protected accessor isPasswordValid: boolean = false
protected accessor password: string = ''
protected accessor passwordConfirmation: string = ''
protected registration = new Registration()
@@ -1,18 +0,0 @@
/* Copyright (c) 2026 The Brave Authors. All rights reserved.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at https://mozilla.org/MPL/2.0/. */
/* #css_wrapper_metadata_start
* #type=style-lit
* #scheme=relative
* #import=./brave_account_common.css.js
* #include=brave-account-common
* #css_wrapper_metadata_end */
#brave-alias-dropdown {
--leo-icon-size: var(--leo-icon-xs);
display: flex;
font: var(--leo-font-small-regular);
gap: var(--leo-spacing-s);
}
@@ -28,10 +28,7 @@ export function getHtml(this: BraveAccountEmailInputElement) {
: ''}"
slot="errors"
>
<div
class="dropdown-content"
id="brave-alias-dropdown"
>
<div class="dropdown-content">
<leo-icon name="warning-triangle-filled"></leo-icon>
<div>$i18n{BRAVE_ACCOUNT_EMAIL_INPUT_ERROR_MESSAGE}</div>
</div>
@@ -5,7 +5,7 @@
import { CrLitElement } from '//resources/lit/v3_0/lit.rollup.js'
import { getCss } from './brave_account_email_input.css.js'
import { getCss } from './brave_account_common.css.js'
import { getHtml } from './brave_account_email_input.html.js'
// Maximum email address length according to this RFC3696 errata:
@@ -1,25 +0,0 @@
/* Copyright (c) 2026 The Brave Authors. All rights reserved.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at https://mozilla.org/MPL/2.0/. */
/* #css_wrapper_metadata_start
* #type=style-lit
* #scheme=relative
* #import=./brave_account_common.css.js
* #include=brave-account-common
* #css_wrapper_metadata_end */
.label-container {
display: flex;
justify-content: space-between;
width: 100%;
}
#password-confirmation-dropdown {
--leo-icon-size: var(--leo-icon-xs);
align-items: center;
display: flex;
font: var(--leo-font-small-regular);
gap: var(--leo-spacing-s);
}
@@ -3,12 +3,13 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at https://mozilla.org/MPL/2.0/. */
import { html, nothing } from '//resources/lit/v3_0/lit.rollup.js'
import { html } from '//resources/lit/v3_0/lit.rollup.js'
import './brave_account_password_icons.js'
import './brave_account_password_strength_meter.js'
import {
BraveAccountPasswordInputElement,
freezeWhen,
MAX_PASSWORD_LENGTH,
} from './brave_account_password_input.js'
@@ -17,7 +18,7 @@ export function getHtml(this: BraveAccountPasswordInputElement) {
<leo-input
maxlength=${MAX_PASSWORD_LENGTH}
placeholder=${this.placeholder}
?showErrors=${this.config.mode !== 'regular'}
showErrors
type="password"
@blur=${this.focusHandler}
@focus=${this.focusHandler}
@@ -40,31 +41,51 @@ export function getHtml(this: BraveAccountPasswordInputElement) {
class="dropdown ${this.shouldShowDropdown ? 'visible' : ''}"
slot="errors"
>
<div class="dropdown-content">
${(() => {
switch (this.config.mode) {
case 'confirmation':
return html`
<div id="password-confirmation-dropdown">
<leo-icon name=${this.getIconName()}></leo-icon>
<!-- Note: .dropdown-content is included in each branch (rather than
wrapping the entire ternary) to ensure the fadeIn animation triggers
on content changes. When Lit replaces one branch with another, it
removes the old .dropdown-content and inserts a new one, causing the
animation to run.
freezeWhen directive freezes the previous content when password
is empty (dropdown collapsing), preventing flashes during animation. -->
${freezeWhen(
this.password.length === 0,
this.config.mode === 'confirmation'
? this.password === this.confirmPassword
? html`
<div class="dropdown-content">
<leo-icon name="check-circle-filled"></leo-icon>
<div>
${this.icon === 'check-circle-filled'
? html`$i18n{BRAVE_ACCOUNT_CONFIRM_PASSWORD_INPUT_SUCCESS_MESSAGE}`
: html`$i18n{BRAVE_ACCOUNT_CONFIRM_PASSWORD_INPUT_ERROR_MESSAGE}`}
$i18n{BRAVE_ACCOUNT_CONFIRM_PASSWORD_INPUT_SUCCESS_MESSAGE}
</div>
</div>
`
case 'strength':
return html`<brave-account-password-strength-meter
password=${this.password}
@password-strength-changed=${this.onPasswordStrengthChanged}
>
</brave-account-password-strength-meter>`
default:
return nothing
}
})()}
</div>
: html`
<div class="dropdown-content">
<leo-icon name="warning-triangle-filled"></leo-icon>
<div>
$i18n{BRAVE_ACCOUNT_CONFIRM_PASSWORD_INPUT_ERROR_MESSAGE}
</div>
</div>
`
: this.config.mode === 'strength' && this.isValid
? html`<div class="dropdown-content">
<brave-account-password-strength-meter
password=${this.password}
@password-strength-changed=${this.onPasswordStrengthChanged}
>
</brave-account-password-strength-meter>
</div>`
: html`
<div class="dropdown-content">
<leo-icon name="warning-triangle-filled"></leo-icon>
<div>
$i18n{BRAVE_ACCOUNT_PASSWORD_INPUT_WHITESPACE_ERROR_MESSAGE}
</div>
</div>
`,
)}
</div>
</leo-input>
<!--_html_template_end_-->`
@@ -3,9 +3,14 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at https://mozilla.org/MPL/2.0/. */
import { CrLitElement } from '//resources/lit/v3_0/lit.rollup.js'
import {
AsyncDirective,
CrLitElement,
directive,
nothing,
} from '//resources/lit/v3_0/lit.rollup.js'
import { getCss } from './brave_account_password_input.css.js'
import { getCss } from './brave_account_common.css.js'
import { getHtml } from './brave_account_password_input.html.js'
import type { PasswordStrengthChangedEventDetail } from './brave_account_password_strength_meter.js'
import type { ToggleVisibilityEventDetail } from './brave_account_password_icons.js'
@@ -16,7 +21,21 @@ import type { ToggleVisibilityEventDetail } from './brave_account_password_icons
// length in code units, not bytes.
export const MAX_PASSWORD_LENGTH = 8192
export type PasswordInputEventDetail = { password: string }
// Custom directive that freezes the previously rendered value when `freeze`
// is true. Similar to Lit's `noChange` (not exported by Chromium's Lit wrapper
// from //third_party/lit/v3_0/lit.ts), but instead of preventing the update,
// it reuses the last rendered value.
class FreezeWhenDirective extends AsyncDirective {
private previousValue: unknown = nothing
render(freeze: boolean, value: unknown): unknown {
return freeze ? this.previousValue : (this.previousValue = value)
}
}
export const freezeWhen = directive(FreezeWhenDirective)
export type PasswordInputEventDetail = { password: string; isValid: boolean }
export type PasswordInputConfig =
| { mode: 'confirmation'; confirmPassword: string }
@@ -60,6 +79,7 @@ export class BraveAccountPasswordInputElement extends CrLitElement {
this.password = detail.value
this.fire('password-input', {
password: this.password,
isValid: this.isValid,
} satisfies PasswordInputEventDetail)
}
@@ -86,28 +106,24 @@ export class BraveAccountPasswordInputElement extends CrLitElement {
protected get shouldStyleAsError() {
return (
this.config.mode === 'confirmation'
&& this.password.length !== 0
&& this.password !== this.confirmPassword
this.password.length !== 0
&& (this.config.mode === 'confirmation'
? this.password !== this.confirmPassword
: this.password !== this.password.trim())
)
}
protected get shouldShowDropdown() {
return this.config.mode !== 'regular' && this.password.length !== 0
return (
this.password.length !== 0
&& (this.config.mode !== 'regular' || !this.isValid)
)
}
protected getIconName() {
if (this.password.length !== 0) {
this.icon =
this.password === this.confirmPassword
? 'check-circle-filled'
: 'warning-triangle-filled'
}
return this.icon
protected get isValid() {
return this.password.length !== 0 && this.password === this.password.trim()
}
protected icon = 'warning-triangle-filled'
protected accessor config: PasswordInputConfig = { mode: 'regular' }
protected accessor isCapsLockOn = false
protected accessor isInputFocused = false
@@ -33,6 +33,7 @@ export function getHtml(this: BraveAccountSignInDialogElement) {
placeholder="$i18n{BRAVE_ACCOUNT_PASSWORD_INPUT_PLACEHOLDER}"
@password-input=${(e: CustomEvent<PasswordInputEventDetail>) => {
this.password = e.detail.password
this.isPasswordValid = e.detail.isValid
}}
>
<div
@@ -35,6 +35,7 @@ export class BraveAccountSignInDialogElement extends CrLitElement {
email: { type: String },
isEmailValid: { type: Boolean },
isCapsLockOn: { type: Boolean },
isPasswordValid: { type: Boolean },
password: { type: String },
}
}
@@ -95,11 +96,8 @@ export class BraveAccountSignInDialogElement extends CrLitElement {
protected accessor email: string = ''
protected accessor isEmailValid: boolean = false
protected accessor isCapsLockOn: boolean = false
protected accessor isPasswordValid: boolean = false
protected accessor password: string = ''
protected get isPasswordValid(): boolean {
return this.password.length !== 0
}
}
declare global {
@@ -180,6 +180,9 @@
<message name="IDS_BRAVE_ACCOUNT_CAPS_LOCK_ON" translateable="false" desc="Tooltip message shown when Caps Lock is enabled on password input fields" formatter_data="webui=BraveAccount">
Caps Lock is on
</message>
<message name="IDS_BRAVE_ACCOUNT_PASSWORD_INPUT_WHITESPACE_ERROR_MESSAGE" translateable="false" desc="Error message shown when password has leading or trailing whitespace" formatter_data="webui=BraveAccount">
Spaces not allowed at the beginning or end of the password.
</message>
<message name="IDS_BRAVE_ACCOUNT_RESEND_CONFIRMATION_EMAIL_SUCCESS_TITLE" translateable="false" desc="Title for the alert shown after successfully resending the confirmation email" formatter_data="webui=BraveAccountSettings android_java">
Done