1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-22 03:03:43 +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:
Todd Martin
2025-04-10 18:58:49 -04:00
committed by GitHub
parent e88813e983
commit f7934b98c6
13 changed files with 241 additions and 295 deletions

View File

@@ -1,43 +1,34 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Observable } from "rxjs";
export abstract class LoginEmailServiceAbstraction {
/**
* An observable that monitors the loginEmail in memory.
* An observable that monitors the loginEmail.
* The loginEmail is the email that is being used in the current login process.
*/
loginEmail$: Observable<string | null>;
abstract loginEmail$: Observable<string | null>;
/**
* An observable that monitors the storedEmail on disk.
* An observable that monitors the remembered email.
* This will return null if an account is being added.
*/
storedEmail$: Observable<string | null>;
abstract rememberedEmail$: Observable<string | null>;
/**
* Sets the loginEmail in memory.
* The loginEmail is the email that is being used in the current login process.
* Consumed through `loginEmail$` observable.
*/
setLoginEmail: (email: string) => Promise<void>;
abstract setLoginEmail: (email: string) => Promise<void>;
/**
* Gets from memory whether or not the email should be stored on disk when `saveEmailSettings` is called.
* @returns A boolean stating whether or not the email should be stored on disk.
* Persist the user's choice of whether to remember their email on subsequent login attempts.
* Consumed through `rememberedEmail$` observable.
*/
getRememberEmail: () => boolean;
abstract setRememberedEmailChoice: (email: string, remember: boolean) => Promise<void>;
/**
* Sets in memory whether or not the email should be stored on disk when `saveEmailSettings` is called.
* Clears the in-progress login email, to be used after a successful login.
*/
setRememberEmail: (value: boolean) => void;
abstract clearLoginEmail: () => Promise<void>;
/**
* Sets the email and rememberEmail properties in memory to null.
* Clears the remembered email.
*/
clearValues: () => void;
/**
* Saves or clears the email on disk
* - If an account is being added, only changes the stored email when rememberEmail is true.
* - If rememberEmail is true, sets the email on disk to the current email.
* - If rememberEmail is false, sets the email on disk to null.
* Always clears the email and rememberEmail properties from memory.
* @returns A promise that resolves once the email settings are saved.
*/
saveEmailSettings: () => Promise<void>;
abstract clearRememberedEmail: () => Promise<void>;
}

View File

@@ -14,7 +14,7 @@ import { UserId } from "@bitwarden/common/types/guid";
import { LoginEmailService, STORED_EMAIL } from "./login-email.service";
describe("LoginEmailService", () => {
let sut: LoginEmailService;
let service: LoginEmailService;
let accountService: FakeAccountService;
let authService: MockProxy<AuthService>;
@@ -34,119 +34,93 @@ describe("LoginEmailService", () => {
mockAuthStatuses$ = new BehaviorSubject<Record<UserId, AuthenticationStatus>>({});
authService.authStatuses$ = mockAuthStatuses$;
sut = new LoginEmailService(accountService, authService, stateProvider);
service = new LoginEmailService(accountService, authService, stateProvider);
});
afterEach(() => {
jest.clearAllMocks();
});
describe("storedEmail$", () => {
it("returns the stored email when not adding an account", async () => {
await sut.setLoginEmail("userEmail@bitwarden.com");
sut.setRememberEmail(true);
await sut.saveEmailSettings();
describe("rememberedEmail$", () => {
it("returns the remembered email when not adding an account", async () => {
const testEmail = "test@bitwarden.com";
const result = await firstValueFrom(sut.storedEmail$);
await service.setRememberedEmailChoice(testEmail, true);
expect(result).toEqual("userEmail@bitwarden.com");
const result = await firstValueFrom(service.rememberedEmail$);
expect(result).toEqual(testEmail);
});
it("returns the stored email when not adding an account and the user has just logged in", async () => {
await sut.setLoginEmail("userEmail@bitwarden.com");
sut.setRememberEmail(true);
await sut.saveEmailSettings();
it("returns the remembered email when not adding an account and the user has just logged in", async () => {
const testEmail = "test@bitwarden.com";
await service.setRememberedEmailChoice(testEmail, true);
mockAuthStatuses$.next({ [userId]: AuthenticationStatus.Unlocked });
// account service already initialized with userId as active user
const result = await firstValueFrom(sut.storedEmail$);
const result = await firstValueFrom(service.rememberedEmail$);
expect(result).toEqual("userEmail@bitwarden.com");
expect(result).toEqual(testEmail);
});
it("returns null when adding an account", async () => {
await sut.setLoginEmail("userEmail@bitwarden.com");
sut.setRememberEmail(true);
await sut.saveEmailSettings();
const testEmail = "test@bitwarden.com";
await service.setRememberedEmailChoice(testEmail, true);
mockAuthStatuses$.next({
[userId]: AuthenticationStatus.Unlocked,
["OtherUserId" as UserId]: AuthenticationStatus.Locked,
});
const result = await firstValueFrom(sut.storedEmail$);
const result = await firstValueFrom(service.rememberedEmail$);
expect(result).toBeNull();
});
});
describe("saveEmailSettings", () => {
it("saves the email when not adding an account", async () => {
await sut.setLoginEmail("userEmail@bitwarden.com");
sut.setRememberEmail(true);
await sut.saveEmailSettings();
describe("setRememberedEmailChoice", () => {
it("sets the remembered email when remember is true", async () => {
const testEmail = "test@bitwarden.com";
await service.setRememberedEmailChoice(testEmail, true);
const result = await firstValueFrom(storedEmailState.state$);
expect(result).toEqual("userEmail@bitwarden.com");
expect(result).toEqual(testEmail);
});
it("clears the email when not adding an account and rememberEmail is false", async () => {
it("clears the remembered email when remember is false", async () => {
storedEmailState.stateSubject.next("initialEmail@bitwarden.com");
await sut.setLoginEmail("userEmail@bitwarden.com");
sut.setRememberEmail(false);
await sut.saveEmailSettings();
const testEmail = "test@bitwarden.com";
await service.setRememberedEmailChoice(testEmail, false);
const result = await firstValueFrom(storedEmailState.state$);
expect(result).toBeNull();
});
});
it("saves the email when adding an account", async () => {
mockAuthStatuses$.next({
[userId]: AuthenticationStatus.Unlocked,
["OtherUserId" as UserId]: AuthenticationStatus.Locked,
});
describe("setLoginEmail", () => {
it("sets the login email", async () => {
const testEmail = "test@bitwarden.com";
await service.setLoginEmail(testEmail);
await sut.setLoginEmail("userEmail@bitwarden.com");
sut.setRememberEmail(true);
await sut.saveEmailSettings();
const result = await firstValueFrom(storedEmailState.state$);
expect(result).toEqual("userEmail@bitwarden.com");
expect(await firstValueFrom(service.loginEmail$)).toEqual(testEmail);
});
});
it("does not clear the email when adding an account and rememberEmail is false", async () => {
storedEmailState.stateSubject.next("initialEmail@bitwarden.com");
describe("clearLoginEmail", () => {
it("clears the login email", async () => {
const testEmail = "test@bitwarden.com";
await service.setLoginEmail(testEmail);
await service.clearLoginEmail();
mockAuthStatuses$.next({
[userId]: AuthenticationStatus.Unlocked,
["OtherUserId" as UserId]: AuthenticationStatus.Locked,
});
await sut.setLoginEmail("userEmail@bitwarden.com");
sut.setRememberEmail(false);
await sut.saveEmailSettings();
const result = await firstValueFrom(storedEmailState.state$);
// result should not be null
expect(result).toEqual("initialEmail@bitwarden.com");
});
it("does not clear the email and rememberEmail after saving", async () => {
// Browser uses these values to maintain the email between login and 2fa components so
// we do not want to clear them too early.
await sut.setLoginEmail("userEmail@bitwarden.com");
sut.setRememberEmail(true);
await sut.saveEmailSettings();
const result = await firstValueFrom(sut.loginEmail$);
expect(result).toBe("userEmail@bitwarden.com");
expect(await firstValueFrom(service.loginEmail$)).toBeNull();
});
});
});

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Observable, firstValueFrom, switchMap } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
@@ -26,8 +24,6 @@ export const STORED_EMAIL = new KeyDefinition<string>(LOGIN_EMAIL_DISK, "storedE
});
export class LoginEmailService implements LoginEmailServiceAbstraction {
private rememberEmail: boolean;
// True if an account is currently being added through account switching
private readonly addingAccount$: Observable<boolean>;
@@ -35,7 +31,7 @@ export class LoginEmailService implements LoginEmailServiceAbstraction {
loginEmail$: Observable<string | null>;
private readonly storedEmailState: GlobalState<string>;
storedEmail$: Observable<string | null>;
rememberedEmail$: Observable<string | null>;
constructor(
private accountService: AccountService,
@@ -60,7 +56,7 @@ export class LoginEmailService implements LoginEmailServiceAbstraction {
this.loginEmail$ = this.loginEmailState.state$;
this.storedEmail$ = this.storedEmailState.state$.pipe(
this.rememberedEmail$ = this.storedEmailState.state$.pipe(
switchMap(async (storedEmail) => {
// When adding an account, we don't show the stored email
if (await firstValueFrom(this.addingAccount$)) {
@@ -71,44 +67,32 @@ export class LoginEmailService implements LoginEmailServiceAbstraction {
);
}
/** Sets the login email in memory.
* The login email is the email that is being used in the current login process.
*/
async setLoginEmail(email: string) {
await this.loginEmailState.update((_) => email);
}
getRememberEmail() {
return this.rememberEmail;
/**
* Clears the in-progress login email from state.
* Note: Only clear on successful login or you are sure they are not needed.
* The extension client uses these values to maintain the email between login and 2fa components so
* we do not want to clear them too early.
*/
async clearLoginEmail() {
await this.loginEmailState.update((_) => null);
}
setRememberEmail(value: boolean) {
this.rememberEmail = value ?? false;
async setRememberedEmailChoice(email: string, remember: boolean): Promise<void> {
if (remember) {
await this.storedEmailState.update((_) => email);
} else {
await this.storedEmailState.update((_) => null);
}
}
// Note: only clear values on successful login or you are sure they are not needed.
// Browser uses these values to maintain the email between login and 2fa components so
// we do not want to clear them too early.
async clearValues() {
await this.setLoginEmail(null);
this.rememberEmail = false;
}
async saveEmailSettings() {
const addingAccount = await firstValueFrom(this.addingAccount$);
const email = await firstValueFrom(this.loginEmail$);
await this.storedEmailState.update((storedEmail) => {
// If we're adding an account, only overwrite the stored email when rememberEmail is true
if (addingAccount) {
if (this.rememberEmail) {
return email;
}
return storedEmail;
}
// Saving with rememberEmail set to false will clear the stored email
if (this.rememberEmail) {
return email;
}
return null;
});
async clearRememberedEmail(): Promise<void> {
await this.storedEmailState.update((_) => null);
}
}

View File

@@ -3,14 +3,17 @@ import { UserId } from "@bitwarden/common/types/guid";
import { UserAsymmetricKeysRegenerationService } from "@bitwarden/key-management";
import { LoginSuccessHandlerService } from "../../abstractions/login-success-handler.service";
import { LoginEmailService } from "../login-email/login-email.service";
export class DefaultLoginSuccessHandlerService implements LoginSuccessHandlerService {
constructor(
private syncService: SyncService,
private userAsymmetricKeysRegenerationService: UserAsymmetricKeysRegenerationService,
private loginEmailService: LoginEmailService,
) {}
async run(userId: UserId): Promise<void> {
await this.syncService.fullSync(true);
await this.userAsymmetricKeysRegenerationService.regenerateIfNeeded(userId);
await this.loginEmailService.clearLoginEmail();
}
}