From 7adc4eaee5be5c27c4b05de9dbf1540be6e404ec Mon Sep 17 00:00:00 2001 From: Alec Rippberger Date: Mon, 7 Apr 2025 10:42:21 -0500 Subject: [PATCH] Refactor TwoFactorFormCacheService --- ...sion-two-factor-form-cache.service.spec.ts | 207 ------------------ ...extension-two-factor-form-cache.service.ts | 93 -------- .../src/popup/services/services.module.ts | 7 - .../src/app/services/services.module.ts | 7 - .../desktop-two-factor-form-cache.service.ts | 30 --- .../core/services/two-factor-auth/index.ts | 1 - .../web-two-factor-form-cache.service.ts | 30 --- apps/web/src/app/core/core.module.ts | 7 - .../two-factor-auth/abstractions/index.ts | 1 - ...o-factor-form-cache.service.abstraction.ts | 54 ----- .../two-factor-auth-email.component.ts | 32 +-- .../auth/src/angular/two-factor-auth/index.ts | 1 - .../two-factor-auth.component.html | 2 + .../two-factor-auth.component.ts | 66 +++--- .../default-two-factor-form-cache.service.ts | 90 ++++++++ .../auth/models/view/two-factor-form.view.ts | 19 ++ 16 files changed, 158 insertions(+), 489 deletions(-) delete mode 100644 apps/browser/src/auth/services/extension-two-factor-form-cache.service.spec.ts delete mode 100644 apps/browser/src/auth/services/extension-two-factor-form-cache.service.ts delete mode 100644 apps/desktop/src/auth/services/desktop-two-factor-form-cache.service.ts delete mode 100644 apps/web/src/app/auth/core/services/two-factor-auth/web-two-factor-form-cache.service.ts delete mode 100644 libs/auth/src/angular/two-factor-auth/abstractions/index.ts delete mode 100644 libs/auth/src/angular/two-factor-auth/abstractions/two-factor-form-cache.service.abstraction.ts create mode 100644 libs/auth/src/common/services/auth-request/default-two-factor-form-cache.service.ts create mode 100644 libs/common/src/auth/models/view/two-factor-form.view.ts diff --git a/apps/browser/src/auth/services/extension-two-factor-form-cache.service.spec.ts b/apps/browser/src/auth/services/extension-two-factor-form-cache.service.spec.ts deleted file mode 100644 index 4601b77857f..00000000000 --- a/apps/browser/src/auth/services/extension-two-factor-form-cache.service.spec.ts +++ /dev/null @@ -1,207 +0,0 @@ -import { signal } from "@angular/core"; -import { TestBed } from "@angular/core/testing"; -import { firstValueFrom } from "rxjs"; - -import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service"; -import { TwoFactorFormData } from "@bitwarden/auth/angular"; -import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; - -import { ExtensionTwoFactorFormCacheService } from "./extension-two-factor-form-cache.service"; - -describe("ExtensionTwoFactorFormCacheService", () => { - let service: ExtensionTwoFactorFormCacheService; - let testBed: TestBed; - const formDataSignal = signal(null); - const getFormDataSignal = jest.fn().mockReturnValue(formDataSignal); - const getFeatureFlag = jest.fn().mockResolvedValue(false); - const formDataSetMock = jest.spyOn(formDataSignal, "set"); - - const mockFormData: TwoFactorFormData = { - token: "123456", - remember: true, - selectedProviderType: TwoFactorProviderType.Authenticator, - emailSent: false, - }; - - beforeEach(() => { - getFormDataSignal.mockClear(); - getFeatureFlag.mockClear(); - formDataSetMock.mockClear(); - - testBed = TestBed.configureTestingModule({ - providers: [ - { provide: ViewCacheService, useValue: { signal: getFormDataSignal } }, - { provide: ConfigService, useValue: { getFeatureFlag } }, - ExtensionTwoFactorFormCacheService, - ], - }); - }); - - describe("feature enabled", () => { - beforeEach(async () => { - getFeatureFlag.mockImplementation((featureFlag: FeatureFlag) => { - if (featureFlag === FeatureFlag.PM9115_TwoFactorExtensionDataPersistence) { - return Promise.resolve(true); - } - return Promise.resolve(false); - }); - - service = testBed.inject(ExtensionTwoFactorFormCacheService); - }); - - describe("isEnabled$", () => { - it("emits true when feature flag is on", async () => { - const result = await firstValueFrom(service.isEnabled$()); - - expect(result).toBe(true); - expect(getFeatureFlag).toHaveBeenCalledWith( - FeatureFlag.PM9115_TwoFactorExtensionDataPersistence, - ); - }); - }); - - describe("isEnabled", () => { - it("returns true when feature flag is on", async () => { - const result = await service.isEnabled(); - - expect(result).toBe(true); - expect(getFeatureFlag).toHaveBeenCalledWith( - FeatureFlag.PM9115_TwoFactorExtensionDataPersistence, - ); - }); - }); - - describe("getFormData", () => { - it("returns cached form data", async () => { - formDataSignal.set(mockFormData); - - const result = await service.getFormData(); - - expect(result).toEqual(mockFormData); - }); - - it("returns null when cache is empty", async () => { - formDataSignal.set(null); - - const result = await service.getFormData(); - - expect(result).toBeNull(); - }); - }); - - describe("formData$", () => { - it("emits cached form data", async () => { - formDataSignal.set(mockFormData); - - const result = await firstValueFrom(service.formData$()); - - expect(result).toEqual(mockFormData); - }); - - it("emits null when cache is empty", async () => { - formDataSignal.set(null); - - const result = await firstValueFrom(service.formData$()); - - expect(result).toBeNull(); - }); - }); - - describe("saveFormData", () => { - it("updates the cached form data", async () => { - await service.saveFormData(mockFormData); - - expect(formDataSetMock).toHaveBeenCalledWith({ ...mockFormData }); - }); - - it("creates a shallow copy of the data", async () => { - const data = { ...mockFormData }; - - await service.saveFormData(data); - - expect(formDataSetMock).toHaveBeenCalledWith(data); - // Should be a new object, not the same reference - expect(formDataSetMock.mock.calls[0][0]).not.toBe(data); - }); - }); - - describe("clearFormData", () => { - it("sets the cache to null", async () => { - await service.clearFormData(); - - expect(formDataSetMock).toHaveBeenCalledWith(null); - }); - }); - }); - - describe("feature disabled", () => { - beforeEach(async () => { - formDataSignal.set(mockFormData); - getFeatureFlag.mockImplementation((featureFlag: FeatureFlag) => { - if (featureFlag === FeatureFlag.PM9115_TwoFactorExtensionDataPersistence) { - return Promise.resolve(false); - } - return Promise.resolve(false); - }); - - service = testBed.inject(ExtensionTwoFactorFormCacheService); - formDataSetMock.mockClear(); - }); - - describe("isEnabled$", () => { - it("emits false when feature flag is off", async () => { - const result = await firstValueFrom(service.isEnabled$()); - - expect(result).toBe(false); - expect(getFeatureFlag).toHaveBeenCalledWith( - FeatureFlag.PM9115_TwoFactorExtensionDataPersistence, - ); - }); - }); - - describe("isEnabled", () => { - it("returns false when feature flag is off", async () => { - const result = await service.isEnabled(); - - expect(result).toBe(false); - expect(getFeatureFlag).toHaveBeenCalledWith( - FeatureFlag.PM9115_TwoFactorExtensionDataPersistence, - ); - }); - }); - - describe("formData$", () => { - it("emits null when feature is disabled", async () => { - const result = await firstValueFrom(service.formData$()); - - expect(result).toBeNull(); - }); - }); - - describe("getFormData", () => { - it("returns null when feature is disabled", async () => { - const result = await service.getFormData(); - - expect(result).toBeNull(); - }); - }); - - describe("saveFormData", () => { - it("does not update cache when feature is disabled", async () => { - await service.saveFormData(mockFormData); - - expect(formDataSetMock).not.toHaveBeenCalled(); - }); - }); - - describe("clearFormData", () => { - it("still works when feature is disabled", async () => { - await service.clearFormData(); - - expect(formDataSetMock).toHaveBeenCalledWith(null); - }); - }); - }); -}); diff --git a/apps/browser/src/auth/services/extension-two-factor-form-cache.service.ts b/apps/browser/src/auth/services/extension-two-factor-form-cache.service.ts deleted file mode 100644 index a3a1d6c2b3a..00000000000 --- a/apps/browser/src/auth/services/extension-two-factor-form-cache.service.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { Injectable, WritableSignal } from "@angular/core"; -import { Observable, of, switchMap, from } from "rxjs"; - -import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service"; -import { TwoFactorFormCacheService, TwoFactorFormData } from "@bitwarden/auth/angular"; -import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; - -const TWO_FACTOR_FORM_CACHE_KEY = "two-factor-form-cache"; - -// Utilize function overloading to create a type-safe deserializer to match the exact expected signature -function deserializeFormData(jsonValue: null): null; -function deserializeFormData(jsonValue: { - token?: string; - remember?: boolean; - selectedProviderType?: TwoFactorProviderType; - emailSent?: boolean; -}): TwoFactorFormData; -function deserializeFormData(jsonValue: any): TwoFactorFormData | null { - if (!jsonValue) { - return null; - } - - return { - token: jsonValue.token, - remember: jsonValue.remember, - selectedProviderType: jsonValue.selectedProviderType, - emailSent: jsonValue.emailSent, - }; -} - -/** - * Service for caching two-factor form data - */ -@Injectable() -export class ExtensionTwoFactorFormCacheService extends TwoFactorFormCacheService { - private formDataCache: WritableSignal; - - constructor( - private viewCacheService: ViewCacheService, - private configService: ConfigService, - ) { - super(); - this.formDataCache = this.viewCacheService.signal({ - key: TWO_FACTOR_FORM_CACHE_KEY, - initialValue: null, - deserializer: deserializeFormData, - }); - } - - /** - * Observable that emits the current enabled state - */ - isEnabled$(): Observable { - return from( - this.configService.getFeatureFlag(FeatureFlag.PM9115_TwoFactorExtensionDataPersistence), - ); - } - - /** - * Observable that emits the current form data - */ - formData$(): Observable { - return this.isEnabled$().pipe( - switchMap((enabled) => { - if (!enabled) { - return of(null); - } - return of(this.formDataCache()); - }), - ); - } - - /** - * Save form data to cache - */ - async saveFormData(data: TwoFactorFormData): Promise { - if (!(await this.isEnabled())) { - return; - } - - // Set the new form data in the cache - this.formDataCache.set({ ...data }); - } - - /** - * Clear form data from cache - */ - async clearFormData(): Promise { - this.formDataCache.set(null); - } -} diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 09675779584..f603f82a5cf 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -31,7 +31,6 @@ import { TwoFactorAuthDuoComponentService, TwoFactorAuthWebAuthnComponentService, SsoComponentService, - TwoFactorFormCacheService, } from "@bitwarden/auth/angular"; import { LockService, @@ -147,7 +146,6 @@ import { ExtensionTwoFactorAuthComponentService } from "../../auth/services/exte import { ExtensionTwoFactorAuthDuoComponentService } from "../../auth/services/extension-two-factor-auth-duo-component.service"; import { ExtensionTwoFactorAuthEmailComponentService } from "../../auth/services/extension-two-factor-auth-email-component.service"; import { ExtensionTwoFactorAuthWebAuthnComponentService } from "../../auth/services/extension-two-factor-auth-webauthn-component.service"; -import { ExtensionTwoFactorFormCacheService } from "../../auth/services/extension-two-factor-form-cache.service"; import { AutofillService as AutofillServiceAbstraction } from "../../autofill/services/abstractions/autofill.service"; import AutofillService from "../../autofill/services/autofill.service"; import { InlineMenuFieldQualificationService } from "../../autofill/services/inline-menu-field-qualification.service"; @@ -566,11 +564,6 @@ const safeProviders: SafeProvider[] = [ useClass: ExtensionTwoFactorAuthWebAuthnComponentService, deps: [], }), - safeProvider({ - provide: TwoFactorFormCacheService, - useClass: ExtensionTwoFactorFormCacheService, - deps: [PopupViewCacheService, ConfigService], - }), safeProvider({ provide: TwoFactorAuthDuoComponentService, useClass: ExtensionTwoFactorAuthDuoComponentService, diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index 700602f5c30..db28f01f27a 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -28,7 +28,6 @@ import { SsoComponentService, DefaultSsoComponentService, TwoFactorAuthDuoComponentService, - TwoFactorFormCacheService, } from "@bitwarden/auth/angular"; import { InternalUserDecryptionOptionsServiceAbstraction, @@ -110,7 +109,6 @@ import { DefaultSshImportPromptService, SshImportPromptService } from "@bitwarde import { DesktopLoginApprovalComponentService } from "../../auth/login/desktop-login-approval-component.service"; import { DesktopLoginComponentService } from "../../auth/login/desktop-login-component.service"; import { DesktopTwoFactorAuthDuoComponentService } from "../../auth/services/desktop-two-factor-auth-duo-component.service"; -import { DesktopTwoFactorFormCacheService } from "../../auth/services/desktop-two-factor-form-cache.service"; import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service"; import { DesktopAutofillService } from "../../autofill/services/desktop-autofill.service"; import { DesktopFido2UserInterfaceService } from "../../autofill/services/desktop-fido2-user-interface.service"; @@ -423,11 +421,6 @@ const safeProviders: SafeProvider[] = [ PlatformUtilsServiceAbstraction, ], }), - safeProvider({ - provide: TwoFactorFormCacheService, - useClass: DesktopTwoFactorFormCacheService, - deps: [], - }), safeProvider({ provide: SdkClientFactory, useClass: flagEnabled("sdk") ? DefaultSdkClientFactory : NoopSdkClientFactory, diff --git a/apps/desktop/src/auth/services/desktop-two-factor-form-cache.service.ts b/apps/desktop/src/auth/services/desktop-two-factor-form-cache.service.ts deleted file mode 100644 index 7af0a0490c9..00000000000 --- a/apps/desktop/src/auth/services/desktop-two-factor-form-cache.service.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Injectable } from "@angular/core"; -import { Observable, of } from "rxjs"; - -import { TwoFactorFormCacheService, TwoFactorFormData } from "@bitwarden/auth/angular"; - -/** - * No-op implementation of TwoFactorFormCacheService for desktop - */ -@Injectable() -export class DesktopTwoFactorFormCacheService extends TwoFactorFormCacheService { - constructor() { - super(); - } - - isEnabled$(): Observable { - return of(false); - } - - formData$(): Observable { - return of(null); - } - - async saveFormData(): Promise { - return Promise.resolve(); - } - - async clearFormData(): Promise { - return Promise.resolve(); - } -} diff --git a/apps/web/src/app/auth/core/services/two-factor-auth/index.ts b/apps/web/src/app/auth/core/services/two-factor-auth/index.ts index 2d20e5fe02a..ba2697fdee4 100644 --- a/apps/web/src/app/auth/core/services/two-factor-auth/index.ts +++ b/apps/web/src/app/auth/core/services/two-factor-auth/index.ts @@ -1,3 +1,2 @@ export * from "./web-two-factor-auth-component.service"; export * from "./web-two-factor-auth-duo-component.service"; -export * from "./web-two-factor-form-cache.service"; diff --git a/apps/web/src/app/auth/core/services/two-factor-auth/web-two-factor-form-cache.service.ts b/apps/web/src/app/auth/core/services/two-factor-auth/web-two-factor-form-cache.service.ts deleted file mode 100644 index 06d1e8555c4..00000000000 --- a/apps/web/src/app/auth/core/services/two-factor-auth/web-two-factor-form-cache.service.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Injectable } from "@angular/core"; -import { Observable, of } from "rxjs"; - -import { TwoFactorFormCacheService, TwoFactorFormData } from "@bitwarden/auth/angular"; - -/** - * No-op implementation of TwoFactorFormCacheService for web app - */ -@Injectable() -export class WebTwoFactorFormCacheService extends TwoFactorFormCacheService { - constructor() { - super(); - } - - isEnabled$(): Observable { - return of(false); - } - - formData$(): Observable { - return of(null); - } - - async saveFormData(): Promise { - return Promise.resolve(); - } - - async clearFormData(): Promise { - return Promise.resolve(); - } -} diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index 694aa2d4325..9e6f88d18d6 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -35,7 +35,6 @@ import { LoginDecryptionOptionsService, TwoFactorAuthComponentService, TwoFactorAuthDuoComponentService, - TwoFactorFormCacheService, } from "@bitwarden/auth/angular"; import { InternalUserDecryptionOptionsServiceAbstraction, @@ -120,7 +119,6 @@ import { LinkSsoService, } from "../auth"; import { WebSsoComponentService } from "../auth/core/services/login/web-sso-component.service"; -import { WebTwoFactorFormCacheService } from "../auth/core/services/two-factor-auth/web-two-factor-form-cache.service"; import { AcceptOrganizationInviteService } from "../auth/organization-invite/accept-organization.service"; import { HtmlStorageService } from "../core/html-storage.service"; import { I18nService } from "../core/i18n.service"; @@ -279,11 +277,6 @@ const safeProviders: SafeProvider[] = [ useClass: WebTwoFactorAuthComponentService, deps: [], }), - safeProvider({ - provide: TwoFactorFormCacheService, - useClass: WebTwoFactorFormCacheService, - deps: [], - }), safeProvider({ provide: SetPasswordJitService, useClass: WebSetPasswordJitService, diff --git a/libs/auth/src/angular/two-factor-auth/abstractions/index.ts b/libs/auth/src/angular/two-factor-auth/abstractions/index.ts deleted file mode 100644 index 9c8185b0b5a..00000000000 --- a/libs/auth/src/angular/two-factor-auth/abstractions/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./two-factor-form-cache.service.abstraction"; diff --git a/libs/auth/src/angular/two-factor-auth/abstractions/two-factor-form-cache.service.abstraction.ts b/libs/auth/src/angular/two-factor-auth/abstractions/two-factor-form-cache.service.abstraction.ts deleted file mode 100644 index d2b6d493c54..00000000000 --- a/libs/auth/src/angular/two-factor-auth/abstractions/two-factor-form-cache.service.abstraction.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { Observable, firstValueFrom } from "rxjs"; - -import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; - -/** - * Interface for two-factor form data - */ -export interface TwoFactorFormData { - token?: string; - remember?: boolean; - selectedProviderType?: TwoFactorProviderType; - emailSent?: boolean; -} - -/** - * Abstract service for two-factor form caching - */ -export abstract class TwoFactorFormCacheService { - /** - * Observable that emits the current enabled state of the feature flag - */ - abstract isEnabled$(): Observable; - - /** - * Helper method that returns whether the feature is enabled - * @returns A promise that resolves to true if the feature is enabled - */ - async isEnabled(): Promise { - return firstValueFrom(this.isEnabled$()); - } - - /** - * Save form data to cache - */ - abstract saveFormData(data: TwoFactorFormData): Promise; - - /** - * Observable that emits the current form data - */ - abstract formData$(): Observable; - - /** - * Helper method to retrieve form data - * @returns A promise that resolves to the form data - */ - async getFormData(): Promise { - return firstValueFrom(this.formData$()); - } - - /** - * Clear form data from cache - */ - abstract clearFormData(): Promise; -} diff --git a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email.component.ts b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email.component.ts index b3741cc0f78..971490ed920 100644 --- a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email.component.ts +++ b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email.component.ts @@ -22,8 +22,6 @@ import { ToastService, } from "@bitwarden/components"; -import { TwoFactorFormCacheService } from "../../abstractions/two-factor-form-cache.service.abstraction"; - import { TwoFactorAuthEmailComponentService } from "./two-factor-auth-email-component.service"; @Component({ @@ -45,7 +43,9 @@ import { TwoFactorAuthEmailComponentService } from "./two-factor-auth-email-comp }) export class TwoFactorAuthEmailComponent implements OnInit { @Input({ required: true }) tokenFormControl: FormControl | undefined = undefined; + @Input({ required: true }) emailSent: boolean = false; @Output() tokenChange = new EventEmitter<{ token: string }>(); + @Output() emailSendEvent = new EventEmitter(); twoFactorEmail: string | undefined = undefined; emailPromise: Promise | undefined; @@ -60,7 +60,6 @@ export class TwoFactorAuthEmailComponent implements OnInit { protected appIdService: AppIdService, private toastService: ToastService, private twoFactorAuthEmailComponentService: TwoFactorAuthEmailComponentService, - private twoFactorFormCacheService: TwoFactorFormCacheService, ) {} async ngOnInit(): Promise { @@ -80,20 +79,15 @@ export class TwoFactorAuthEmailComponent implements OnInit { this.twoFactorEmail = email2faProviderData.Email; - // Check if email has already been sent according to the cache - let emailAlreadySent = false; - try { - const cachedData = await this.twoFactorFormCacheService.getFormData(); - emailAlreadySent = cachedData?.emailSent === true; - } catch (e) { - this.logService.error(e); - } - - if (!emailAlreadySent) { + if (!this.emailSent) { await this.sendEmail(false); } } + /** + * Emits the token value to the parent component + * @param event - The event object from the input field + */ onTokenChange(event: Event) { const tokenValue = (event.target as HTMLInputElement).value || ""; this.tokenChange.emit({ token: tokenValue }); @@ -130,17 +124,7 @@ export class TwoFactorAuthEmailComponent implements OnInit { this.emailPromise = this.apiService.postTwoFactorEmail(request); await this.emailPromise; - // Update cache to indicate email was sent - try { - const cachedData = (await this.twoFactorFormCacheService.getFormData()) || {}; - await this.twoFactorFormCacheService.saveFormData({ - ...cachedData, - emailSent: true, - token: undefined, - }); - } catch (e) { - this.logService.error(e); - } + this.emailSendEvent.emit(); if (doToast) { this.toastService.showToast({ diff --git a/libs/auth/src/angular/two-factor-auth/index.ts b/libs/auth/src/angular/two-factor-auth/index.ts index cd4f7b2228c..c5dc7b1a59d 100644 --- a/libs/auth/src/angular/two-factor-auth/index.ts +++ b/libs/auth/src/angular/two-factor-auth/index.ts @@ -2,6 +2,5 @@ export * from "./two-factor-auth-component.service"; export * from "./default-two-factor-auth-component.service"; export * from "./two-factor-auth.component"; export * from "./two-factor-auth.guard"; -export * from "./abstractions"; export * from "./child-components"; diff --git a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.html b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.html index e0c2ffc642a..3192978e818 100644 --- a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.html +++ b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.html @@ -14,6 +14,8 @@ diff --git a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts index f5f46666139..035584e8ed5 100644 --- a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts +++ b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts @@ -46,6 +46,7 @@ import { ToastService, } from "@bitwarden/components"; +import { DefaultTwoFactorFormCacheService } from "../../common/services/auth-request/default-two-factor-form-cache.service"; import { AnonLayoutWrapperDataService } from "../anon-layout/anon-layout-wrapper-data.service"; import { TwoFactorAuthAuthenticatorIcon, @@ -55,7 +56,6 @@ import { TwoFactorAuthDuoIcon, } from "../icons/two-factor-auth"; -import { TwoFactorFormCacheService } from "./abstractions"; import { TwoFactorAuthAuthenticatorComponent } from "./child-components/two-factor-auth-authenticator.component"; import { TwoFactorAuthDuoComponent } from "./child-components/two-factor-auth-duo/two-factor-auth-duo.component"; import { TwoFactorAuthEmailComponent } from "./child-components/two-factor-auth-email/two-factor-auth-email.component"; @@ -101,7 +101,11 @@ interface TwoFactorFormCacheData { TwoFactorAuthYubikeyComponent, TwoFactorAuthWebAuthnComponent, ], - providers: [], + providers: [ + { + provide: DefaultTwoFactorFormCacheService, + }, + ], }) export class TwoFactorAuthComponent implements OnInit, OnDestroy { @ViewChild("continueButton", { read: ElementRef, static: false }) continueButton: @@ -110,6 +114,11 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy { loading = true; + /** + * Whether the email has been sent according to the cache + */ + emailSent = false; + orgSsoIdentifier: string | undefined = undefined; providerType = TwoFactorProviderType; @@ -171,7 +180,7 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy { private anonLayoutWrapperDataService: AnonLayoutWrapperDataService, private environmentService: EnvironmentService, private loginSuccessHandlerService: LoginSuccessHandlerService, - private twoFactorFormCacheService: TwoFactorFormCacheService, + private twoFactorFormCacheService: DefaultTwoFactorFormCacheService, ) {} async ngOnInit() { @@ -180,9 +189,12 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy { this.listenForAuthnSessionTimeout(); + // Initialize the cache + await this.twoFactorFormCacheService.init(); + // Load persisted form data if available let loadedCachedProviderType = false; - const persistedData = await this.twoFactorFormCacheService.getFormData(); + const persistedData = this.twoFactorFormCacheService.getCachedTwoFactorFormData(); if (persistedData) { if (persistedData.token) { this.form.patchValue({ token: persistedData.token }); @@ -194,6 +206,9 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy { this.selectedProviderType = persistedData.selectedProviderType; loadedCachedProviderType = true; } + if (persistedData.emailSent !== undefined) { + this.emailSent = persistedData.emailSent; + } } // Only set default 2FA provider type if we don't have one from cache @@ -218,20 +233,17 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy { */ async saveFormDataWithPartialData(data: Partial) { // Get current cached data - const currentData = (await this.twoFactorFormCacheService.getFormData()) || {}; + const currentData = this.twoFactorFormCacheService.getCachedTwoFactorFormData(); - // Only update fields that are present in the data object - const updatedData: TwoFactorFormCacheData = { - ...currentData, - ...Object.entries(data).reduce((acc, [key, value]) => { - if (value !== undefined) { - acc[key] = value; - } - return acc; - }, {} as any), - }; - - await this.twoFactorFormCacheService.saveFormData(updatedData); + this.twoFactorFormCacheService.cacheTwoFactorFormData({ + token: data?.token ?? currentData?.token ?? "", + remember: data?.remember ?? currentData?.remember ?? false, + selectedProviderType: + data?.selectedProviderType ?? + currentData?.selectedProviderType ?? + TwoFactorProviderType.Authenticator, + emailSent: data?.emailSent ?? currentData?.emailSent ?? false, + }); } /** @@ -335,7 +347,7 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy { const rememberValue = remember ?? this.rememberFormControl.value ?? false; // Persist form data before submitting - await this.twoFactorFormCacheService.saveFormData({ + this.twoFactorFormCacheService.cacheTwoFactorFormData({ token: tokenValue, remember: rememberValue, selectedProviderType: this.selectedProviderType, @@ -363,11 +375,11 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy { async selectOtherTwoFactorMethod() { // Persist current form data before navigating to another method - await this.twoFactorFormCacheService.saveFormData({ - token: undefined, - remember: undefined, + this.twoFactorFormCacheService.cacheTwoFactorFormData({ + token: "", + remember: false, selectedProviderType: this.selectedProviderType, - emailSent: this.selectedProviderType === TwoFactorProviderType.Email, + emailSent: false, }); const dialogRef = TwoFactorOptionsComponent.open(this.dialogService); @@ -384,11 +396,11 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy { await this.setAnonLayoutDataByTwoFactorProviderType(); // Update the persisted provider type when a new one is chosen - await this.twoFactorFormCacheService.saveFormData({ - token: undefined, - remember: undefined, + this.twoFactorFormCacheService.cacheTwoFactorFormData({ + token: "", + remember: false, selectedProviderType: response.type, - emailSent: false, // Reset email sent state when switching providers + emailSent: false, }); this.form.reset(); @@ -469,7 +481,7 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy { private async handleAuthResult(authResult: AuthResult) { // Clear form cache - await this.twoFactorFormCacheService.clearFormData(); + this.twoFactorFormCacheService.clearCachedTwoFactorFormData(); if (await this.handleMigrateEncryptionKey(authResult)) { return; // stop login process diff --git a/libs/auth/src/common/services/auth-request/default-two-factor-form-cache.service.ts b/libs/auth/src/common/services/auth-request/default-two-factor-form-cache.service.ts new file mode 100644 index 00000000000..17be444b718 --- /dev/null +++ b/libs/auth/src/common/services/auth-request/default-two-factor-form-cache.service.ts @@ -0,0 +1,90 @@ +import { inject, Injectable, WritableSignal } from "@angular/core"; + +import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service"; +import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; +import { TwoFactorFormView } from "@bitwarden/common/auth/models/view/two-factor-form.view"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; + +const TWO_FACTOR_FORM_CACHE_KEY = "two-factor-form-cache"; + +export interface TwoFactorFormData { + token?: string; + remember?: boolean; + selectedProviderType?: TwoFactorProviderType; + emailSent?: boolean; +} + +/** + * This is a cache service used for the login via auth request component. + * + * There is sensitive information stored temporarily here. Cache will be cleared + * after 2 minutes. + */ +@Injectable() +export class DefaultTwoFactorFormCacheService { + private viewCacheService: ViewCacheService = inject(ViewCacheService); + private configService: ConfigService = inject(ConfigService); + + /** True when the `PM9115_TwoFactorExtensionDataPersistence` flag is enabled */ + private featureEnabled: boolean = false; + + /** + * Signal for the cached TwoFactorFormData. + */ + private defaultTwoFactorFormCache: WritableSignal = + this.viewCacheService.signal({ + key: TWO_FACTOR_FORM_CACHE_KEY, + initialValue: null, + deserializer: TwoFactorFormView.fromJSON, + }); + + constructor() {} + + /** + * Must be called once before interacting with the cached data, otherwise methods will be noop. + */ + async init() { + this.featureEnabled = await this.configService.getFeatureFlag( + FeatureFlag.PM9115_TwoFactorExtensionDataPersistence, + ); + } + + /** + * Update the cache with the new TwoFactorFormData. + */ + cacheTwoFactorFormData(data: TwoFactorFormData): void { + if (!this.featureEnabled) { + return; + } + + this.defaultTwoFactorFormCache.set({ + token: data.token, + remember: data.remember, + selectedProviderType: data.selectedProviderType, + emailSent: data.emailSent, + } as TwoFactorFormView); + } + + /** + * Clears the cached TwoFactorFormData. + */ + clearCachedTwoFactorFormData(): void { + if (!this.featureEnabled) { + return; + } + + this.defaultTwoFactorFormCache.set(null); + } + + /** + * Returns the cached TwoFactorFormData when available. + */ + getCachedTwoFactorFormData(): TwoFactorFormView | null { + if (!this.featureEnabled) { + return null; + } + + return this.defaultTwoFactorFormCache(); + } +} diff --git a/libs/common/src/auth/models/view/two-factor-form.view.ts b/libs/common/src/auth/models/view/two-factor-form.view.ts new file mode 100644 index 00000000000..482a3f0c5fa --- /dev/null +++ b/libs/common/src/auth/models/view/two-factor-form.view.ts @@ -0,0 +1,19 @@ +import { Jsonify } from "type-fest"; + +import { View } from "@bitwarden/common/models/view/view"; + +import { TwoFactorProviderType } from "../../enums/two-factor-provider-type"; + +/** + * This is a cache model for the two factor form. + */ +export class TwoFactorFormView implements View { + token: string | undefined = undefined; + remember: boolean | undefined = undefined; + selectedProviderType: TwoFactorProviderType | undefined = undefined; + emailSent: boolean | undefined = undefined; + + static fromJSON(obj: Partial>): TwoFactorFormView { + return Object.assign(new TwoFactorFormView(), obj); + } +}