mirror of
https://github.com/bitwarden/browser.git
synced 2025-01-27 22:11:38 +01: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:
parent
ea10c29024
commit
26f086368b
libs/auth/src/angular/login
@ -20,8 +20,8 @@
|
||||
formControlName="email"
|
||||
bitInput
|
||||
appAutofocus
|
||||
(blur)="onEmailBlur($event)"
|
||||
(keyup.enter)="continue()"
|
||||
(input)="onEmailInput($event)"
|
||||
(keyup.enter)="continuePressed()"
|
||||
/>
|
||||
</bit-form-field>
|
||||
|
||||
@ -33,7 +33,7 @@
|
||||
|
||||
<div class="tw-grid tw-gap-3">
|
||||
<!-- Continue button -->
|
||||
<button type="button" bitButton block buttonType="primary" (click)="continue()">
|
||||
<button type="button" bitButton block buttonType="primary" (click)="continuePressed()">
|
||||
{{ "continue" | i18n }}
|
||||
</button>
|
||||
|
||||
@ -54,33 +54,10 @@
|
||||
</ng-container>
|
||||
|
||||
<!-- Button to Login with SSO -->
|
||||
<ng-container *ngIf="clientType === ClientType.Web">
|
||||
<a
|
||||
bitButton
|
||||
block
|
||||
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>
|
||||
<button type="button" bitButton block buttonType="secondary" (click)="handleSsoClick()">
|
||||
<i class="bwi bwi-provider tw-mr-1"></i>
|
||||
{{ "useSingleSignOn" | i18n }}
|
||||
</button>
|
||||
</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 { Component, ElementRef, NgZone, OnDestroy, OnInit, ViewChild } from "@angular/core";
|
||||
import { FormBuilder, FormControl, ReactiveFormsModule, Validators } from "@angular/forms";
|
||||
@ -12,7 +10,6 @@ import {
|
||||
LoginStrategyServiceAbstraction,
|
||||
LoginSuccessHandlerService,
|
||||
PasswordLoginCredentials,
|
||||
RegisterRouteService,
|
||||
} from "@bitwarden/auth/common";
|
||||
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";
|
||||
@ -72,16 +69,15 @@ export enum LoginUiState {
|
||||
],
|
||||
})
|
||||
export class LoginComponent implements OnInit, OnDestroy {
|
||||
@ViewChild("masterPasswordInputRef") masterPasswordInputRef: ElementRef;
|
||||
@ViewChild("masterPasswordInputRef") masterPasswordInputRef: ElementRef | undefined;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
private enforcedMasterPasswordOptions: MasterPasswordPolicyOptions = undefined;
|
||||
private enforcedMasterPasswordOptions: MasterPasswordPolicyOptions | undefined = undefined;
|
||||
readonly Icons = { WaveIcon, VaultIcon };
|
||||
|
||||
clientType: ClientType;
|
||||
ClientType = ClientType;
|
||||
LoginUiState = LoginUiState;
|
||||
registerRoute$ = this.registerRouteService.registerRoute$(); // TODO: remove when email verification flag is removed
|
||||
isKnownDevice = false;
|
||||
loginUiState: LoginUiState = LoginUiState.EMAIL_ENTRY;
|
||||
|
||||
@ -97,13 +93,13 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||
{ updateOn: "submit" },
|
||||
);
|
||||
|
||||
get emailFormControl(): FormControl<string> {
|
||||
get emailFormControl(): FormControl<string | null> {
|
||||
return this.formGroup.controls.email;
|
||||
}
|
||||
|
||||
// Web properties
|
||||
enforcedPasswordPolicyOptions: MasterPasswordPolicyOptions;
|
||||
policies: Policy[];
|
||||
enforcedPasswordPolicyOptions: MasterPasswordPolicyOptions | undefined;
|
||||
policies: Policy[] | undefined;
|
||||
showResetPasswordAutoEnrollWarning = false;
|
||||
|
||||
// Desktop properties
|
||||
@ -125,7 +121,6 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||
private passwordStrengthService: PasswordStrengthServiceAbstraction,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private policyService: InternalPolicyService,
|
||||
private registerRouteService: RegisterRouteService,
|
||||
private router: Router,
|
||||
private toastService: ToastService,
|
||||
private logService: LogService,
|
||||
@ -200,12 +195,12 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
|
||||
const credentials = new PasswordLoginCredentials(
|
||||
email,
|
||||
masterPassword,
|
||||
null, // captcha no longer used in new login / registration scenarios
|
||||
null,
|
||||
);
|
||||
if (!email || !masterPassword) {
|
||||
this.logService.error("Email and master password are required");
|
||||
return;
|
||||
}
|
||||
|
||||
const credentials = new PasswordLoginCredentials(email, masterPassword);
|
||||
|
||||
try {
|
||||
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> {
|
||||
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> {
|
||||
@ -337,9 +337,14 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||
|
||||
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(
|
||||
masterPassword,
|
||||
this.formGroup.value.email,
|
||||
this.formGroup.value.email ?? undefined,
|
||||
)?.score;
|
||||
|
||||
return !this.policyService.evaluateMasterPassword(
|
||||
@ -363,6 +368,7 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||
|
||||
protected async validateEmail(): Promise<boolean> {
|
||||
this.formGroup.controls.email.markAsTouched();
|
||||
this.formGroup.controls.email.updateValueAndValidity({ onlySelf: true, emitEvent: true });
|
||||
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
|
||||
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.
|
||||
* @param event The event object from the input field.
|
||||
*/
|
||||
onEmailBlur(event: Event) {
|
||||
onEmailInput(event: Event) {
|
||||
const emailInput = event.target as HTMLInputElement;
|
||||
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(this.formGroup.value.email);
|
||||
this.loginEmailService.setLoginEmail(emailInput.value);
|
||||
}
|
||||
|
||||
isLoginWithPasskeySupported() {
|
||||
@ -428,28 +436,36 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||
await this.router.navigateByUrl("/hint");
|
||||
}
|
||||
|
||||
protected async goToRegister(): Promise<void> {
|
||||
// TODO: remove when email verification flag is removed
|
||||
const registerRoute = await firstValueFrom(this.registerRoute$);
|
||||
|
||||
if (this.emailFormControl.valid) {
|
||||
await this.router.navigate([registerRoute], {
|
||||
queryParams: { email: this.emailFormControl.value },
|
||||
});
|
||||
protected async saveEmailSettings(): Promise<void> {
|
||||
const email = this.formGroup.value.email;
|
||||
if (!email) {
|
||||
this.logService.error("Email is required to save email settings.");
|
||||
return;
|
||||
}
|
||||
|
||||
await this.router.navigate([registerRoute]);
|
||||
}
|
||||
|
||||
protected async saveEmailSettings(): Promise<void> {
|
||||
await this.loginEmailService.setLoginEmail(this.formGroup.value.email);
|
||||
this.loginEmailService.setRememberEmail(this.formGroup.value.rememberEmail);
|
||||
await this.loginEmailService.setLoginEmail(email);
|
||||
this.loginEmailService.setRememberEmail(this.formGroup.value.rememberEmail ?? false);
|
||||
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> {
|
||||
if (await this.validateEmail()) {
|
||||
const isEmailValid = await this.validateEmail();
|
||||
|
||||
if (isEmailValid) {
|
||||
await this.toggleLoginUiState(LoginUiState.MASTER_PASSWORD_ENTRY);
|
||||
}
|
||||
}
|
||||
@ -460,6 +476,11 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||
* @param email - The user's email
|
||||
*/
|
||||
private async getKnownDevice(email: string): Promise<void> {
|
||||
if (!email) {
|
||||
this.isKnownDevice = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const deviceIdentifier = await this.appIdService.getAppId();
|
||||
this.isKnownDevice = await this.devicesApiService.getKnownDevice(email, deviceIdentifier);
|
||||
@ -503,7 +524,7 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||
const orgPolicies = await this.loginComponentService.getOrgPolicies();
|
||||
|
||||
this.policies = orgPolicies?.policies;
|
||||
this.showResetPasswordAutoEnrollWarning = orgPolicies?.isPolicyAndAutoEnrollEnabled;
|
||||
this.showResetPasswordAutoEnrollWarning = orgPolicies?.isPolicyAndAutoEnrollEnabled ?? 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
|
||||
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
|
||||
// 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.
|
||||
*/
|
||||
protected async backButtonClicked() {
|
||||
// Replace the history so the "forward" button doesn't show (which wouldn't do anything)
|
||||
history.pushState(null, "", window.location.pathname);
|
||||
await this.toggleLoginUiState(LoginUiState.EMAIL_ENTRY);
|
||||
history.back();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
private handlePopState = (event: PopStateEvent) => {
|
||||
private handlePopState = async (event: PopStateEvent) => {
|
||||
if (this.loginUiState === LoginUiState.MASTER_PASSWORD_ENTRY) {
|
||||
// Prevent default navigation
|
||||
// Prevent default navigation when the browser's back button is clicked
|
||||
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
|
||||
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",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user