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:
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user