Account: disallow leading/trailing whitespace in passwords (#35133)
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user