mirror of
https://github.com/bitwarden/browser
synced 2025-12-16 16:23:44 +00:00
fix(login): [PM-11502] Support Remember Email option consistently
* Moved saving of SSO email outside of browser/desktop code * Clarified comments. * Tests * Refactored login component services to manage state * Fixed input on login component * Fixed tests * Linting * Moved web setting in state into web override * updated tests * Fixed typing. * Fixed type safety issues. * Added comments and renamed for clarity. * Removed method parameters that weren't used * Added clarifying comments * Added more comments. * Removed test that is not necessary on base * Test cleanup * More comments. * Linting * Fixed test. * Fixed base URL * Fixed typechecking. * Type checking * Moved setting of email state to default service * Added comments. * Consolidated SSO URL formatting * Updated comment * Fixed reference. * Fixed missing parameter. * Initialized service. * Added comments * Added initialization of new service * Made email optional due to CLI. * Fixed comment on handleSsoClick. * Added SSO email persistence to v1 component. * Updated login email service. * Updated setting of remember me * Removed unnecessary input checking and rearranged functions * Fixed name * Added handling of Remember Email to old component for passkey click * Updated v1 component to persist the email on Continue click * Fix merge conflicts. * Merge conflicts in login component. * Persisted login email on v1 browser component. * Merge conflicts * fix(snap) [PM-17464][PM-17463][PM-15587] Allow Snap to use custom callback protocol * Removed Snap from custom protocol workaround * Fixed tests. * Updated case numbers on test * Resolved PR feedback. * PM-11502 - LoginEmailSvcAbstraction - mark methods as abstract to satisfy strict ts. * Removed test * Changed to persist on leaving fields instead of button click. * Fixed type checking. --------- Co-authored-by: Bernd Schoolmann <mail@quexten.com> Co-authored-by: Jared Snider <jsnider@bitwarden.com> Co-authored-by: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com>
This commit is contained in:
@@ -148,6 +148,62 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
private async defaultOnInit(): Promise<void> {
|
||||
let paramEmailIsSet = false;
|
||||
|
||||
const params = await firstValueFrom(this.activatedRoute.queryParams);
|
||||
|
||||
if (params) {
|
||||
const qParamsEmail = params.email;
|
||||
|
||||
// If there is an email in the query params, set that email as the form field value
|
||||
if (qParamsEmail != null && qParamsEmail.indexOf("@") > -1) {
|
||||
this.formGroup.controls.email.setValue(qParamsEmail);
|
||||
paramEmailIsSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
// If there are no params or no email in the query params, loadEmailSettings from state
|
||||
if (!paramEmailIsSet) {
|
||||
await this.loadRememberedEmail();
|
||||
}
|
||||
|
||||
// Check to see if the device is known so that we can show the Login with Device option
|
||||
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
|
||||
if (!this.activatedRoute) {
|
||||
await this.loadRememberedEmail();
|
||||
}
|
||||
}
|
||||
|
||||
private async desktopOnInit(): Promise<void> {
|
||||
// TODO: refactor to not use deprecated broadcaster service.
|
||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
|
||||
this.ngZone.run(() => {
|
||||
switch (message.command) {
|
||||
case "windowIsFocused":
|
||||
if (this.deferFocus === null) {
|
||||
this.deferFocus = !message.windowIsFocused;
|
||||
if (!this.deferFocus) {
|
||||
this.focusInput();
|
||||
}
|
||||
} else if (this.deferFocus && message.windowIsFocused) {
|
||||
this.focusInput();
|
||||
this.deferFocus = false;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.messagingService.send("getWindowIsFocused");
|
||||
}
|
||||
|
||||
submit = async (): Promise<void> => {
|
||||
if (this.clientType === ClientType.Desktop) {
|
||||
if (this.loginUiState !== LoginUiState.MASTER_PASSWORD_ENTRY) {
|
||||
@@ -172,7 +228,6 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||
try {
|
||||
const authResult = await this.loginStrategyService.logIn(credentials);
|
||||
|
||||
await this.saveEmailSettings();
|
||||
await this.handleAuthResult(authResult);
|
||||
} catch (error) {
|
||||
this.logService.error(error);
|
||||
@@ -250,7 +305,6 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||
|
||||
// User logged in successfully so execute side effects
|
||||
await this.loginSuccessHandlerService.run(authResult.userId);
|
||||
this.loginEmailService.clearValues();
|
||||
|
||||
// Determine where to send the user next
|
||||
if (authResult.forcePasswordReset != ForceSetPasswordReason.None) {
|
||||
@@ -288,7 +342,6 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||
await this.router.navigate(["vault"]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the master password meets the enforced policy requirements
|
||||
* and if the user is required to change their password.
|
||||
@@ -344,11 +397,10 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.saveEmailSettings();
|
||||
await this.router.navigate(["/login-with-device"]);
|
||||
}
|
||||
|
||||
protected async validateEmail(): Promise<boolean> {
|
||||
protected async emailIsValid(): Promise<boolean> {
|
||||
this.formGroup.controls.email.markAsTouched();
|
||||
this.formGroup.controls.email.updateValueAndValidity({ onlySelf: true, emitEvent: true });
|
||||
return this.formGroup.controls.email.valid;
|
||||
@@ -399,37 +451,14 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the email value from the input field.
|
||||
* @param event The event object from the input field.
|
||||
*/
|
||||
onEmailInput(event: Event) {
|
||||
const emailInput = event.target as HTMLInputElement;
|
||||
this.formGroup.controls.email.setValue(emailInput.value);
|
||||
this.loginEmailService.setLoginEmail(emailInput.value);
|
||||
}
|
||||
|
||||
isLoginWithPasskeySupported() {
|
||||
return this.loginComponentService.isLoginWithPasskeySupported();
|
||||
}
|
||||
|
||||
protected async goToHint(): Promise<void> {
|
||||
await this.saveEmailSettings();
|
||||
await this.router.navigateByUrl("/hint");
|
||||
}
|
||||
|
||||
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.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.
|
||||
@@ -445,13 +474,44 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||
* Continue to the master password entry state (only if email is validated)
|
||||
*/
|
||||
protected async continue(): Promise<void> {
|
||||
const isEmailValid = await this.validateEmail();
|
||||
const isEmailValid = await this.emailIsValid();
|
||||
|
||||
if (isEmailValid) {
|
||||
await this.toggleLoginUiState(LoginUiState.MASTER_PASSWORD_ENTRY);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the Login with Passkey button click.
|
||||
* We need a handler here in order to persist the remember email selection to state before routing.
|
||||
* @param event - The event object.
|
||||
*/
|
||||
async handleLoginWithPasskeyClick() {
|
||||
await this.router.navigate(["/login-with-passkey"]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the SSO button click.
|
||||
* @param event - The event object.
|
||||
*/
|
||||
async handleSsoClick() {
|
||||
// Make sure the email is valid
|
||||
const isEmailValid = await this.emailIsValid();
|
||||
if (!isEmailValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure the email is not empty, for type safety
|
||||
const email = this.formGroup.value.email;
|
||||
if (!email) {
|
||||
this.logService.error("Email is required for SSO");
|
||||
return;
|
||||
}
|
||||
|
||||
// Send the user to SSO, either through routing or through redirecting to the web app
|
||||
await this.loginComponentService.redirectToSsoLogin(email);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call to check if the device is known.
|
||||
* Known means that the user has logged in with this device before.
|
||||
@@ -473,23 +533,17 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
private async loadEmailSettings(): Promise<void> {
|
||||
// Try to load the email from memory first
|
||||
const email = await firstValueFrom(this.loginEmailService.loginEmail$);
|
||||
const rememberEmail = this.loginEmailService.getRememberEmail();
|
||||
|
||||
if (email) {
|
||||
this.formGroup.controls.email.setValue(email);
|
||||
this.formGroup.controls.rememberEmail.setValue(rememberEmail);
|
||||
/**
|
||||
* Check to see if the user has remembered an email on the current device.
|
||||
* If so, set the email in the form field and set rememberEmail to true. If not, set rememberEmail to false.
|
||||
*/
|
||||
private async loadRememberedEmail(): Promise<void> {
|
||||
const storedEmail = await firstValueFrom(this.loginEmailService.rememberedEmail$);
|
||||
if (storedEmail) {
|
||||
this.formGroup.controls.email.setValue(storedEmail);
|
||||
this.formGroup.controls.rememberEmail.setValue(true);
|
||||
} else {
|
||||
// If there is no email in memory, check for a storedEmail on disk
|
||||
const storedEmail = await firstValueFrom(this.loginEmailService.storedEmail$);
|
||||
|
||||
if (storedEmail) {
|
||||
this.formGroup.controls.email.setValue(storedEmail);
|
||||
// If there is a storedEmail, rememberEmail defaults to true
|
||||
this.formGroup.controls.rememberEmail.setValue(true);
|
||||
}
|
||||
this.formGroup.controls.rememberEmail.setValue(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -503,62 +557,6 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||
?.focus();
|
||||
}
|
||||
|
||||
private async defaultOnInit(): Promise<void> {
|
||||
let paramEmailIsSet = false;
|
||||
|
||||
const params = await firstValueFrom(this.activatedRoute.queryParams);
|
||||
|
||||
if (params) {
|
||||
const qParamsEmail = params.email;
|
||||
|
||||
// If there is an email in the query params, set that email as the form field value
|
||||
if (qParamsEmail != null && qParamsEmail.indexOf("@") > -1) {
|
||||
this.formGroup.controls.email.setValue(qParamsEmail);
|
||||
paramEmailIsSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
// If there are no params or no email in the query params, loadEmailSettings from state
|
||||
if (!paramEmailIsSet) {
|
||||
await this.loadEmailSettings();
|
||||
}
|
||||
|
||||
// Check to see if the device is known so that we can show the Login with Device option
|
||||
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
|
||||
if (!this.activatedRoute) {
|
||||
await this.loadEmailSettings();
|
||||
}
|
||||
}
|
||||
|
||||
private async desktopOnInit(): Promise<void> {
|
||||
// TODO: refactor to not use deprecated broadcaster service.
|
||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
|
||||
this.ngZone.run(() => {
|
||||
switch (message.command) {
|
||||
case "windowIsFocused":
|
||||
if (this.deferFocus === null) {
|
||||
this.deferFocus = !message.windowIsFocused;
|
||||
if (!this.deferFocus) {
|
||||
this.focusInput();
|
||||
}
|
||||
} else if (this.deferFocus && message.windowIsFocused) {
|
||||
this.focusInput();
|
||||
this.deferFocus = false;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.messagingService.send("getWindowIsFocused");
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to determine if the back button should be shown.
|
||||
* @returns true if the back button should be shown.
|
||||
@@ -597,27 +595,46 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle the SSO button click.
|
||||
* Persist the entered email address and the user's choice to remember it to state.
|
||||
*/
|
||||
async handleSsoClick() {
|
||||
const email = this.formGroup.value.email;
|
||||
|
||||
// Make sure the email is valid
|
||||
const isEmailValid = await this.validateEmail();
|
||||
if (!isEmailValid) {
|
||||
return;
|
||||
private async persistEmailIfValid(): Promise<void> {
|
||||
if (await this.emailIsValid()) {
|
||||
const email = this.formGroup.value.email;
|
||||
const rememberEmail = this.formGroup.value.rememberEmail ?? false;
|
||||
if (!email) {
|
||||
return;
|
||||
}
|
||||
await this.loginEmailService.setLoginEmail(email);
|
||||
await this.loginEmailService.setRememberedEmailChoice(email, rememberEmail);
|
||||
} else {
|
||||
await this.loginEmailService.clearLoginEmail();
|
||||
await this.loginEmailService.clearRememberedEmail();
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure the email is not empty, for type safety
|
||||
if (!email) {
|
||||
this.logService.error("Email is required for SSO");
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* Set the email value from the input field.
|
||||
* We only update the form controls onSubmit instead of onBlur because we don't want to show validation errors until
|
||||
* the user submits. This is because currently our validation errors are shown below the input fields, and
|
||||
* displaying them causes the screen to "jump".
|
||||
* @param event The event object from the input field.
|
||||
*/
|
||||
async onEmailInput(event: Event) {
|
||||
const emailInput = event.target as HTMLInputElement;
|
||||
this.formGroup.controls.email.setValue(emailInput.value);
|
||||
await this.persistEmailIfValid();
|
||||
}
|
||||
|
||||
// Save the email configuration for the login component
|
||||
await this.saveEmailSettings();
|
||||
|
||||
// Send the user to SSO, either through routing or through redirecting to the web app
|
||||
await this.loginComponentService.redirectToSsoLogin(email);
|
||||
/**
|
||||
* Set the Remember Email value from the input field.
|
||||
* We only update the form controls onSubmit instead of onBlur because we don't want to show validation errors until
|
||||
* the user submits. This is because currently our validation errors are shown below the input fields, and
|
||||
* displaying them causes the screen to "jump".
|
||||
* @param event The event object from the input field.
|
||||
*/
|
||||
async onRememberEmailInput(event: Event) {
|
||||
const rememberEmailInput = event.target as HTMLInputElement;
|
||||
this.formGroup.controls.rememberEmail.setValue(rememberEmailInput.checked);
|
||||
await this.persistEmailIfValid();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user