mirror of
https://github.com/bitwarden/browser
synced 2025-12-10 21:33:27 +00:00
fix(auth): [PM-15987] improve email/master password entry back/forward navigation
- Fix back button behavior in Safari to reliably return to email entry screen - Enable browser forward button after navigating back to email entry - Move email validation to input event instead of blur - Add continueClicked function to differentiate user clicks vs browser navigation - Add email verification gate to SSO route - Enhance master password validation logic - Fix strict typing errors Resolves PM-15987
This commit is contained in:
@@ -20,8 +20,8 @@
|
|||||||
formControlName="email"
|
formControlName="email"
|
||||||
bitInput
|
bitInput
|
||||||
appAutofocus
|
appAutofocus
|
||||||
(blur)="onEmailBlur($event)"
|
(input)="onEmailInput($event)"
|
||||||
(keyup.enter)="continue()"
|
(keyup.enter)="continuePressed()"
|
||||||
/>
|
/>
|
||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
|
|
||||||
<div class="tw-grid tw-gap-3">
|
<div class="tw-grid tw-gap-3">
|
||||||
<!-- Continue button -->
|
<!-- Continue button -->
|
||||||
<button type="button" bitButton block buttonType="primary" (click)="continue()">
|
<button type="button" bitButton block buttonType="primary" (click)="continuePressed()">
|
||||||
{{ "continue" | i18n }}
|
{{ "continue" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -54,33 +54,10 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<!-- Button to Login with SSO -->
|
<!-- Button to Login with SSO -->
|
||||||
<ng-container *ngIf="clientType === ClientType.Web">
|
<button type="button" bitButton block buttonType="secondary" (click)="handleSsoClick()">
|
||||||
<a
|
<i class="bwi bwi-provider tw-mr-1"></i>
|
||||||
bitButton
|
{{ "useSingleSignOn" | i18n }}
|
||||||
block
|
</button>
|
||||||
buttonType="secondary"
|
|
||||||
routerLink="/sso"
|
|
||||||
[queryParams]="formGroup.value.email ? { email: formGroup.value.email } : {}"
|
|
||||||
(click)="saveEmailSettings()"
|
|
||||||
>
|
|
||||||
<i class="bwi bwi-provider tw-mr-1"></i>
|
|
||||||
{{ "useSingleSignOn" | i18n }}
|
|
||||||
</a>
|
|
||||||
</ng-container>
|
|
||||||
<ng-container *ngIf="clientType === ClientType.Browser || clientType === ClientType.Desktop">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
bitButton
|
|
||||||
block
|
|
||||||
buttonType="secondary"
|
|
||||||
(click)="
|
|
||||||
launchSsoBrowserWindow(clientType === ClientType.Browser ? 'browser' : 'desktop')
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<i class="bwi bwi-provider tw-mr-1"></i>
|
|
||||||
{{ "useSingleSignOn" | i18n }}
|
|
||||||
</button>
|
|
||||||
</ng-container>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
|
||||||
// @ts-strict-ignore
|
|
||||||
import { CommonModule } from "@angular/common";
|
import { CommonModule } from "@angular/common";
|
||||||
import { Component, ElementRef, NgZone, OnDestroy, OnInit, ViewChild } from "@angular/core";
|
import { Component, ElementRef, NgZone, OnDestroy, OnInit, ViewChild } from "@angular/core";
|
||||||
import { FormBuilder, FormControl, ReactiveFormsModule, Validators } from "@angular/forms";
|
import { FormBuilder, FormControl, ReactiveFormsModule, Validators } from "@angular/forms";
|
||||||
@@ -12,7 +10,6 @@ import {
|
|||||||
LoginStrategyServiceAbstraction,
|
LoginStrategyServiceAbstraction,
|
||||||
LoginSuccessHandlerService,
|
LoginSuccessHandlerService,
|
||||||
PasswordLoginCredentials,
|
PasswordLoginCredentials,
|
||||||
RegisterRouteService,
|
|
||||||
} from "@bitwarden/auth/common";
|
} from "@bitwarden/auth/common";
|
||||||
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||||
@@ -72,16 +69,15 @@ export enum LoginUiState {
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class LoginComponent implements OnInit, OnDestroy {
|
export class LoginComponent implements OnInit, OnDestroy {
|
||||||
@ViewChild("masterPasswordInputRef") masterPasswordInputRef: ElementRef;
|
@ViewChild("masterPasswordInputRef") masterPasswordInputRef: ElementRef | undefined;
|
||||||
|
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
private enforcedMasterPasswordOptions: MasterPasswordPolicyOptions = undefined;
|
private enforcedMasterPasswordOptions: MasterPasswordPolicyOptions | undefined = undefined;
|
||||||
readonly Icons = { WaveIcon, VaultIcon };
|
readonly Icons = { WaveIcon, VaultIcon };
|
||||||
|
|
||||||
clientType: ClientType;
|
clientType: ClientType;
|
||||||
ClientType = ClientType;
|
ClientType = ClientType;
|
||||||
LoginUiState = LoginUiState;
|
LoginUiState = LoginUiState;
|
||||||
registerRoute$ = this.registerRouteService.registerRoute$(); // TODO: remove when email verification flag is removed
|
|
||||||
isKnownDevice = false;
|
isKnownDevice = false;
|
||||||
loginUiState: LoginUiState = LoginUiState.EMAIL_ENTRY;
|
loginUiState: LoginUiState = LoginUiState.EMAIL_ENTRY;
|
||||||
|
|
||||||
@@ -97,13 +93,13 @@ export class LoginComponent implements OnInit, OnDestroy {
|
|||||||
{ updateOn: "submit" },
|
{ updateOn: "submit" },
|
||||||
);
|
);
|
||||||
|
|
||||||
get emailFormControl(): FormControl<string> {
|
get emailFormControl(): FormControl<string | null> {
|
||||||
return this.formGroup.controls.email;
|
return this.formGroup.controls.email;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Web properties
|
// Web properties
|
||||||
enforcedPasswordPolicyOptions: MasterPasswordPolicyOptions;
|
enforcedPasswordPolicyOptions: MasterPasswordPolicyOptions | undefined;
|
||||||
policies: Policy[];
|
policies: Policy[] | undefined;
|
||||||
showResetPasswordAutoEnrollWarning = false;
|
showResetPasswordAutoEnrollWarning = false;
|
||||||
|
|
||||||
// Desktop properties
|
// Desktop properties
|
||||||
@@ -125,7 +121,6 @@ export class LoginComponent implements OnInit, OnDestroy {
|
|||||||
private passwordStrengthService: PasswordStrengthServiceAbstraction,
|
private passwordStrengthService: PasswordStrengthServiceAbstraction,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private policyService: InternalPolicyService,
|
private policyService: InternalPolicyService,
|
||||||
private registerRouteService: RegisterRouteService,
|
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private toastService: ToastService,
|
private toastService: ToastService,
|
||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
@@ -200,12 +195,12 @@ export class LoginComponent implements OnInit, OnDestroy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const credentials = new PasswordLoginCredentials(
|
if (!email || !masterPassword) {
|
||||||
email,
|
this.logService.error("Email and master password are required");
|
||||||
masterPassword,
|
return;
|
||||||
null, // captcha no longer used in new login / registration scenarios
|
}
|
||||||
null,
|
|
||||||
);
|
const credentials = new PasswordLoginCredentials(email, masterPassword);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await this.loginStrategyService.logIn(credentials);
|
const authResult = await this.loginStrategyService.logIn(credentials);
|
||||||
@@ -301,7 +296,12 @@ export class LoginComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async launchSsoBrowserWindow(clientId: "browser" | "desktop"): Promise<void> {
|
protected async launchSsoBrowserWindow(clientId: "browser" | "desktop"): Promise<void> {
|
||||||
await this.loginComponentService.launchSsoBrowserWindow(this.emailFormControl.value, clientId);
|
const email = this.emailFormControl.value;
|
||||||
|
if (!email) {
|
||||||
|
this.logService.error("Email is required for SSO login");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.loginComponentService.launchSsoBrowserWindow(email, clientId);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async evaluatePassword(): Promise<void> {
|
protected async evaluatePassword(): Promise<void> {
|
||||||
@@ -337,9 +337,14 @@ export class LoginComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
const masterPassword = this.formGroup.controls.masterPassword.value;
|
const masterPassword = this.formGroup.controls.masterPassword.value;
|
||||||
|
|
||||||
|
// Return false if masterPassword is null/undefined since this is only evaluated after successful login
|
||||||
|
if (!masterPassword) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const passwordStrength = this.passwordStrengthService.getPasswordStrength(
|
const passwordStrength = this.passwordStrengthService.getPasswordStrength(
|
||||||
masterPassword,
|
masterPassword,
|
||||||
this.formGroup.value.email,
|
this.formGroup.value.email ?? undefined,
|
||||||
)?.score;
|
)?.score;
|
||||||
|
|
||||||
return !this.policyService.evaluateMasterPassword(
|
return !this.policyService.evaluateMasterPassword(
|
||||||
@@ -363,6 +368,7 @@ export class LoginComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
protected async validateEmail(): Promise<boolean> {
|
protected async validateEmail(): Promise<boolean> {
|
||||||
this.formGroup.controls.email.markAsTouched();
|
this.formGroup.controls.email.markAsTouched();
|
||||||
|
this.formGroup.controls.email.updateValueAndValidity({ onlySelf: true, emitEvent: true });
|
||||||
return this.formGroup.controls.email.valid;
|
return this.formGroup.controls.email.valid;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,7 +410,10 @@ export class LoginComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check to see if the device is known so we can show the Login with Device option
|
// Check to see if the device is known so we can show the Login with Device option
|
||||||
await this.getKnownDevice(this.emailFormControl.value);
|
const email = this.emailFormControl.value;
|
||||||
|
if (email) {
|
||||||
|
await this.getKnownDevice(email);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -412,11 +421,10 @@ export class LoginComponent implements OnInit, OnDestroy {
|
|||||||
* Set the email value from the input field.
|
* Set the email value from the input field.
|
||||||
* @param event The event object from the input field.
|
* @param event The event object from the input field.
|
||||||
*/
|
*/
|
||||||
onEmailBlur(event: Event) {
|
onEmailInput(event: Event) {
|
||||||
const emailInput = event.target as HTMLInputElement;
|
const emailInput = event.target as HTMLInputElement;
|
||||||
this.formGroup.controls.email.setValue(emailInput.value);
|
this.formGroup.controls.email.setValue(emailInput.value);
|
||||||
// Call setLoginEmail so that the email is pre-populated when navigating to the "enter password" screen.
|
this.loginEmailService.setLoginEmail(emailInput.value);
|
||||||
this.loginEmailService.setLoginEmail(this.formGroup.value.email);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoginWithPasskeySupported() {
|
isLoginWithPasskeySupported() {
|
||||||
@@ -428,28 +436,36 @@ export class LoginComponent implements OnInit, OnDestroy {
|
|||||||
await this.router.navigateByUrl("/hint");
|
await this.router.navigateByUrl("/hint");
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async goToRegister(): Promise<void> {
|
protected async saveEmailSettings(): Promise<void> {
|
||||||
// TODO: remove when email verification flag is removed
|
const email = this.formGroup.value.email;
|
||||||
const registerRoute = await firstValueFrom(this.registerRoute$);
|
if (!email) {
|
||||||
|
this.logService.error("Email is required to save email settings.");
|
||||||
if (this.emailFormControl.valid) {
|
|
||||||
await this.router.navigate([registerRoute], {
|
|
||||||
queryParams: { email: this.emailFormControl.value },
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.router.navigate([registerRoute]);
|
await this.loginEmailService.setLoginEmail(email);
|
||||||
}
|
this.loginEmailService.setRememberEmail(this.formGroup.value.rememberEmail ?? false);
|
||||||
|
|
||||||
protected async saveEmailSettings(): Promise<void> {
|
|
||||||
await this.loginEmailService.setLoginEmail(this.formGroup.value.email);
|
|
||||||
this.loginEmailService.setRememberEmail(this.formGroup.value.rememberEmail);
|
|
||||||
await this.loginEmailService.saveEmailSettings();
|
await this.loginEmailService.saveEmailSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Continue button clicked (or enter key pressed).
|
||||||
|
* Adds the login url to the browser's history so that the back button can be used to go back to the email entry state.
|
||||||
|
* Needs to be separate from the continue() function because that can be triggered by the browser's forward button.
|
||||||
|
*/
|
||||||
|
protected async continuePressed() {
|
||||||
|
// Add a new entry to the browser's history so that there is a history entry to go back to
|
||||||
|
history.pushState({}, "", window.location.href);
|
||||||
|
await this.continue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Continue to the master password entry state (only if email is validated)
|
||||||
|
*/
|
||||||
protected async continue(): Promise<void> {
|
protected async continue(): Promise<void> {
|
||||||
if (await this.validateEmail()) {
|
const isEmailValid = await this.validateEmail();
|
||||||
|
|
||||||
|
if (isEmailValid) {
|
||||||
await this.toggleLoginUiState(LoginUiState.MASTER_PASSWORD_ENTRY);
|
await this.toggleLoginUiState(LoginUiState.MASTER_PASSWORD_ENTRY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -460,6 +476,11 @@ export class LoginComponent implements OnInit, OnDestroy {
|
|||||||
* @param email - The user's email
|
* @param email - The user's email
|
||||||
*/
|
*/
|
||||||
private async getKnownDevice(email: string): Promise<void> {
|
private async getKnownDevice(email: string): Promise<void> {
|
||||||
|
if (!email) {
|
||||||
|
this.isKnownDevice = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const deviceIdentifier = await this.appIdService.getAppId();
|
const deviceIdentifier = await this.appIdService.getAppId();
|
||||||
this.isKnownDevice = await this.devicesApiService.getKnownDevice(email, deviceIdentifier);
|
this.isKnownDevice = await this.devicesApiService.getKnownDevice(email, deviceIdentifier);
|
||||||
@@ -503,7 +524,7 @@ export class LoginComponent implements OnInit, OnDestroy {
|
|||||||
const orgPolicies = await this.loginComponentService.getOrgPolicies();
|
const orgPolicies = await this.loginComponentService.getOrgPolicies();
|
||||||
|
|
||||||
this.policies = orgPolicies?.policies;
|
this.policies = orgPolicies?.policies;
|
||||||
this.showResetPasswordAutoEnrollWarning = orgPolicies?.isPolicyAndAutoEnrollEnabled;
|
this.showResetPasswordAutoEnrollWarning = orgPolicies?.isPolicyAndAutoEnrollEnabled ?? false;
|
||||||
|
|
||||||
let paramEmailIsSet = false;
|
let paramEmailIsSet = false;
|
||||||
|
|
||||||
@@ -525,7 +546,9 @@ export class LoginComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check to see if the device is known so that we can show the Login with Device option
|
// Check to see if the device is known so that we can show the Login with Device option
|
||||||
await this.getKnownDevice(this.emailFormControl.value);
|
if (this.emailFormControl.value) {
|
||||||
|
await this.getKnownDevice(this.emailFormControl.value);
|
||||||
|
}
|
||||||
|
|
||||||
// Backup check to handle unknown case where activatedRoute is not available
|
// Backup check to handle unknown case where activatedRoute is not available
|
||||||
// This shouldn't happen under normal circumstances
|
// This shouldn't happen under normal circumstances
|
||||||
@@ -573,23 +596,50 @@ export class LoginComponent implements OnInit, OnDestroy {
|
|||||||
* Handle the back button click to transition back to the email entry state.
|
* Handle the back button click to transition back to the email entry state.
|
||||||
*/
|
*/
|
||||||
protected async backButtonClicked() {
|
protected async backButtonClicked() {
|
||||||
// Replace the history so the "forward" button doesn't show (which wouldn't do anything)
|
history.back();
|
||||||
history.pushState(null, "", window.location.pathname);
|
|
||||||
await this.toggleLoginUiState(LoginUiState.EMAIL_ENTRY);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle the popstate event to transition back to the email entry state when the back button is clicked.
|
* Handle the popstate event to transition back to the email entry state when the back button is clicked.
|
||||||
|
* Also handles the case where the user clicks the forward button.
|
||||||
* @param event - The popstate event.
|
* @param event - The popstate event.
|
||||||
*/
|
*/
|
||||||
private handlePopState = (event: PopStateEvent) => {
|
private handlePopState = async (event: PopStateEvent) => {
|
||||||
if (this.loginUiState === LoginUiState.MASTER_PASSWORD_ENTRY) {
|
if (this.loginUiState === LoginUiState.MASTER_PASSWORD_ENTRY) {
|
||||||
// Prevent default navigation
|
// Prevent default navigation when the browser's back button is clicked
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
// Replace the history so the "forward" button doesn't show (which wouldn't do anything)
|
|
||||||
history.pushState(null, "", window.location.pathname);
|
|
||||||
// Transition back to email entry state
|
// Transition back to email entry state
|
||||||
void this.toggleLoginUiState(LoginUiState.EMAIL_ENTRY);
|
void this.toggleLoginUiState(LoginUiState.EMAIL_ENTRY);
|
||||||
|
} else if (this.loginUiState === LoginUiState.EMAIL_ENTRY) {
|
||||||
|
// Prevent default navigation when the browser's forward button is clicked
|
||||||
|
event.preventDefault();
|
||||||
|
// Continue to the master password entry state
|
||||||
|
await this.continue();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the SSO button click.
|
||||||
|
* @param event - The event object.
|
||||||
|
*/
|
||||||
|
async handleSsoClick() {
|
||||||
|
const isEmailValid = await this.validateEmail();
|
||||||
|
|
||||||
|
if (!isEmailValid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.saveEmailSettings();
|
||||||
|
|
||||||
|
if (this.clientType === ClientType.Web) {
|
||||||
|
await this.router.navigate(["/sso"], {
|
||||||
|
queryParams: { email: this.formGroup.value.email },
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.launchSsoBrowserWindow(
|
||||||
|
this.clientType === ClientType.Browser ? "browser" : "desktop",
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user