1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-15 07:43:35 +00:00

chore(feature-flag) [PM-22604] Remove 2FA persistence feature flag

* Removed flag.

* Fixed tests to no longer reference flag.

* Fixed test.

* Removed duplicate test class.

* Moved files into folders for yubikey and authenticator

* Removed TwoFactorAuthEmailComponentService since it is no longer needed

* Removed export

* Fixed export
This commit is contained in:
Todd Martin
2025-07-10 15:00:49 -04:00
committed by GitHub
parent 318040233c
commit c5be837b51
21 changed files with 9 additions and 581 deletions

View File

@@ -1,3 +1,2 @@
export * from "./two-factor-auth-email";
export * from "./two-factor-auth-duo";
export * from "./two-factor-auth-webauthn";

View File

@@ -1,6 +0,0 @@
import { TwoFactorAuthEmailComponentService } from "./two-factor-auth-email-component.service";
export class DefaultTwoFactorAuthEmailComponentService
implements TwoFactorAuthEmailComponentService {
// no default implementation
}

View File

@@ -1,2 +0,0 @@
export * from "./default-two-factor-auth-email-component.service";
export * from "./two-factor-auth-email-component.service";

View File

@@ -1,165 +0,0 @@
import { TestBed } from "@angular/core/testing";
import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject } from "rxjs";
import { ViewCacheService } from "@bitwarden/angular/platform/view-cache";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import {
TwoFactorAuthEmailComponentCache,
TwoFactorAuthEmailComponentCacheService,
} from "./two-factor-auth-email-component-cache.service";
describe("TwoFactorAuthEmailCache", () => {
describe("fromJSON", () => {
it("returns null when input is null", () => {
const result = TwoFactorAuthEmailComponentCache.fromJSON(null as any);
expect(result).toBeNull();
});
it("creates a TwoFactorAuthEmailCache instance from valid JSON", () => {
const jsonData = { emailSent: true };
const result = TwoFactorAuthEmailComponentCache.fromJSON(jsonData);
expect(result).not.toBeNull();
expect(result).toBeInstanceOf(TwoFactorAuthEmailComponentCache);
expect(result?.emailSent).toBe(true);
});
});
});
describe("TwoFactorAuthEmailComponentCacheService", () => {
let service: TwoFactorAuthEmailComponentCacheService;
let mockViewCacheService: MockProxy<ViewCacheService>;
let mockConfigService: MockProxy<ConfigService>;
let cacheData: BehaviorSubject<TwoFactorAuthEmailComponentCache | null>;
let mockSignal: any;
beforeEach(() => {
mockViewCacheService = mock<ViewCacheService>();
mockConfigService = mock<ConfigService>();
cacheData = new BehaviorSubject<TwoFactorAuthEmailComponentCache | null>(null);
mockSignal = jest.fn(() => cacheData.getValue());
mockSignal.set = jest.fn((value: TwoFactorAuthEmailComponentCache | null) =>
cacheData.next(value),
);
mockViewCacheService.signal.mockReturnValue(mockSignal);
TestBed.configureTestingModule({
providers: [
TwoFactorAuthEmailComponentCacheService,
{ provide: ViewCacheService, useValue: mockViewCacheService },
{ provide: ConfigService, useValue: mockConfigService },
],
});
service = TestBed.inject(TwoFactorAuthEmailComponentCacheService);
});
it("creates the service", () => {
expect(service).toBeTruthy();
});
describe("init", () => {
it("sets featureEnabled to true when flag is enabled", async () => {
mockConfigService.getFeatureFlag.mockResolvedValue(true);
await service.init();
expect(mockConfigService.getFeatureFlag).toHaveBeenCalledWith(
FeatureFlag.PM9115_TwoFactorExtensionDataPersistence,
);
service.cacheData({ emailSent: true });
expect(mockSignal.set).toHaveBeenCalled();
});
it("sets featureEnabled to false when flag is disabled", async () => {
mockConfigService.getFeatureFlag.mockResolvedValue(false);
await service.init();
expect(mockConfigService.getFeatureFlag).toHaveBeenCalledWith(
FeatureFlag.PM9115_TwoFactorExtensionDataPersistence,
);
service.cacheData({ emailSent: true });
expect(mockSignal.set).not.toHaveBeenCalled();
});
});
describe("cacheData", () => {
beforeEach(async () => {
mockConfigService.getFeatureFlag.mockResolvedValue(true);
await service.init();
});
it("caches email sent state when feature is enabled", () => {
service.cacheData({ emailSent: true });
expect(mockSignal.set).toHaveBeenCalledWith({
emailSent: true,
});
});
it("does not cache data when feature is disabled", async () => {
mockConfigService.getFeatureFlag.mockResolvedValue(false);
await service.init();
service.cacheData({ emailSent: true });
expect(mockSignal.set).not.toHaveBeenCalled();
});
});
describe("clearCachedData", () => {
beforeEach(async () => {
mockConfigService.getFeatureFlag.mockResolvedValue(true);
await service.init();
});
it("clears cached data when feature is enabled", () => {
service.clearCachedData();
expect(mockSignal.set).toHaveBeenCalledWith(null);
});
it("does not clear cached data when feature is disabled", async () => {
mockConfigService.getFeatureFlag.mockResolvedValue(false);
await service.init();
service.clearCachedData();
expect(mockSignal.set).not.toHaveBeenCalled();
});
});
describe("getCachedData", () => {
beforeEach(async () => {
mockConfigService.getFeatureFlag.mockResolvedValue(true);
await service.init();
});
it("returns cached data when feature is enabled", () => {
const testData = new TwoFactorAuthEmailComponentCache();
testData.emailSent = true;
cacheData.next(testData);
const result = service.getCachedData();
expect(result).toEqual(testData);
expect(mockSignal).toHaveBeenCalled();
});
it("returns null when feature is disabled", async () => {
mockConfigService.getFeatureFlag.mockResolvedValue(false);
await service.init();
const result = service.getCachedData();
expect(result).toBeNull();
expect(mockSignal).not.toHaveBeenCalled();
});
});
});

View File

@@ -3,7 +3,6 @@ import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject } from "rxjs";
import { ViewCacheService } from "@bitwarden/angular/platform/view-cache";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import {
@@ -61,87 +60,26 @@ describe("TwoFactorAuthEmailComponentCacheService", () => {
expect(service).toBeTruthy();
});
describe("init", () => {
it("sets featureEnabled to true when flag is enabled", async () => {
mockConfigService.getFeatureFlag.mockResolvedValue(true);
await service.init();
expect(mockConfigService.getFeatureFlag).toHaveBeenCalledWith(
FeatureFlag.PM9115_TwoFactorExtensionDataPersistence,
);
service.cacheData({ emailSent: true });
expect(mockSignal.set).toHaveBeenCalled();
});
it("sets featureEnabled to false when flag is disabled", async () => {
mockConfigService.getFeatureFlag.mockResolvedValue(false);
await service.init();
expect(mockConfigService.getFeatureFlag).toHaveBeenCalledWith(
FeatureFlag.PM9115_TwoFactorExtensionDataPersistence,
);
service.cacheData({ emailSent: true });
expect(mockSignal.set).not.toHaveBeenCalled();
});
});
describe("cacheData", () => {
beforeEach(async () => {
mockConfigService.getFeatureFlag.mockResolvedValue(true);
await service.init();
});
it("caches email sent state when feature is enabled", () => {
it("caches email sent state", () => {
service.cacheData({ emailSent: true });
expect(mockSignal.set).toHaveBeenCalledWith({
emailSent: true,
});
});
it("does not cache data when feature is disabled", async () => {
mockConfigService.getFeatureFlag.mockResolvedValue(false);
await service.init();
service.cacheData({ emailSent: true });
expect(mockSignal.set).not.toHaveBeenCalled();
});
});
describe("clearCachedData", () => {
beforeEach(async () => {
mockConfigService.getFeatureFlag.mockResolvedValue(true);
await service.init();
});
it("clears cached data when feature is enabled", () => {
it("clears cached data", () => {
service.clearCachedData();
expect(mockSignal.set).toHaveBeenCalledWith(null);
});
it("does not clear cached data when feature is disabled", async () => {
mockConfigService.getFeatureFlag.mockResolvedValue(false);
await service.init();
service.clearCachedData();
expect(mockSignal.set).not.toHaveBeenCalled();
});
});
describe("getCachedData", () => {
beforeEach(async () => {
mockConfigService.getFeatureFlag.mockResolvedValue(true);
await service.init();
});
it("returns cached data when feature is enabled", () => {
it("returns cached data", () => {
const testData = new TwoFactorAuthEmailComponentCache();
testData.emailSent = true;
cacheData.next(testData);
@@ -151,15 +89,5 @@ describe("TwoFactorAuthEmailComponentCacheService", () => {
expect(result).toEqual(testData);
expect(mockSignal).toHaveBeenCalled();
});
it("returns null when feature is disabled", async () => {
mockConfigService.getFeatureFlag.mockResolvedValue(false);
await service.init();
const result = service.getCachedData();
expect(result).toBeNull();
expect(mockSignal).not.toHaveBeenCalled();
});
});
});

View File

@@ -2,8 +2,6 @@ import { inject, Injectable, WritableSignal } from "@angular/core";
import { Jsonify } from "type-fest";
import { ViewCacheService } from "@bitwarden/angular/platform/view-cache";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
/**
* The key for the email two factor auth component cache.
@@ -34,10 +32,6 @@ export class TwoFactorAuthEmailComponentCache {
@Injectable()
export class TwoFactorAuthEmailComponentCacheService {
private viewCacheService: ViewCacheService = inject(ViewCacheService);
private configService: ConfigService = inject(ConfigService);
/** True when the feature flag is enabled */
private featureEnabled: boolean = false;
/**
* Signal for the cached email state.
@@ -49,23 +43,10 @@ export class TwoFactorAuthEmailComponentCacheService {
deserializer: TwoFactorAuthEmailComponentCache.fromJSON,
});
/**
* Must be called once before interacting with the cached data.
*/
async init() {
this.featureEnabled = await this.configService.getFeatureFlag(
FeatureFlag.PM9115_TwoFactorExtensionDataPersistence,
);
}
/**
* Cache the email sent state.
*/
cacheData(data: { emailSent: boolean }): void {
if (!this.featureEnabled) {
return;
}
this.emailCache.set({
emailSent: data.emailSent,
} as TwoFactorAuthEmailComponentCache);
@@ -75,10 +56,6 @@ export class TwoFactorAuthEmailComponentCacheService {
* Clear the cached email data.
*/
clearCachedData(): void {
if (!this.featureEnabled) {
return;
}
this.emailCache.set(null);
}
@@ -86,10 +63,6 @@ export class TwoFactorAuthEmailComponentCacheService {
* Get whether the email has been sent.
*/
getCachedData(): TwoFactorAuthEmailComponentCache | null {
if (!this.featureEnabled) {
return null;
}
return this.emailCache();
}
}

View File

@@ -1,10 +0,0 @@
/**
* A service that manages all cross client functionality for the email 2FA component.
*/
export abstract class TwoFactorAuthEmailComponentService {
/**
* Optionally shows a warning to the user that they might need to popout the
* window to complete email 2FA.
*/
abstract openPopoutIfApprovedForEmail2fa?(): Promise<void>;
}

View File

@@ -25,7 +25,6 @@ import {
} from "@bitwarden/components";
import { TwoFactorAuthEmailComponentCacheService } from "./two-factor-auth-email-component-cache.service";
import { TwoFactorAuthEmailComponentService } from "./two-factor-auth-email-component.service";
@Component({
selector: "app-two-factor-auth-email",
@@ -66,14 +65,10 @@ export class TwoFactorAuthEmailComponent implements OnInit {
protected apiService: ApiService,
protected appIdService: AppIdService,
private toastService: ToastService,
private twoFactorAuthEmailComponentService: TwoFactorAuthEmailComponentService,
private cacheService: TwoFactorAuthEmailComponentCacheService,
) {}
async ngOnInit(): Promise<void> {
await this.twoFactorAuthEmailComponentService.openPopoutIfApprovedForEmail2fa?.();
await this.cacheService.init();
// Check if email was already sent
const cachedData = this.cacheService.getCachedData();
if (cachedData?.emailSent) {

View File

@@ -4,8 +4,6 @@ import { BehaviorSubject } from "rxjs";
import { ViewCacheService } from "@bitwarden/angular/platform/view-cache";
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 {
TwoFactorAuthComponentCache,
@@ -40,13 +38,11 @@ describe("TwoFactorAuthCache", () => {
describe("TwoFactorAuthComponentCacheService", () => {
let service: TwoFactorAuthComponentCacheService;
let mockViewCacheService: MockProxy<ViewCacheService>;
let mockConfigService: MockProxy<ConfigService>;
let cacheData: BehaviorSubject<TwoFactorAuthComponentCache | null>;
let mockSignal: any;
beforeEach(() => {
mockViewCacheService = mock<ViewCacheService>();
mockConfigService = mock<ConfigService>();
cacheData = new BehaviorSubject<TwoFactorAuthComponentCache | null>(null);
mockSignal = jest.fn(() => cacheData.getValue());
mockSignal.set = jest.fn((value: TwoFactorAuthComponentCache | null) => cacheData.next(value));
@@ -56,7 +52,6 @@ describe("TwoFactorAuthComponentCacheService", () => {
providers: [
TwoFactorAuthComponentCacheService,
{ provide: ViewCacheService, useValue: mockViewCacheService },
{ provide: ConfigService, useValue: mockConfigService },
],
});
@@ -67,41 +62,8 @@ describe("TwoFactorAuthComponentCacheService", () => {
expect(service).toBeTruthy();
});
describe("init", () => {
it("sets featureEnabled to true when flag is enabled", async () => {
mockConfigService.getFeatureFlag.mockResolvedValue(true);
await service.init();
expect(mockConfigService.getFeatureFlag).toHaveBeenCalledWith(
FeatureFlag.PM9115_TwoFactorExtensionDataPersistence,
);
service.cacheData({ token: "123456" });
expect(mockSignal.set).toHaveBeenCalled();
});
it("sets featureEnabled to false when flag is disabled", async () => {
mockConfigService.getFeatureFlag.mockResolvedValue(false);
await service.init();
expect(mockConfigService.getFeatureFlag).toHaveBeenCalledWith(
FeatureFlag.PM9115_TwoFactorExtensionDataPersistence,
);
service.cacheData({ token: "123456" });
expect(mockSignal.set).not.toHaveBeenCalled();
});
});
describe("cacheData", () => {
beforeEach(async () => {
mockConfigService.getFeatureFlag.mockResolvedValue(true);
await service.init();
});
it("caches complete data when feature is enabled", () => {
it("caches complete data", () => {
const testData: TwoFactorAuthComponentData = {
token: "123456",
remember: true,
@@ -117,7 +79,7 @@ describe("TwoFactorAuthComponentCacheService", () => {
});
});
it("caches partial data when feature is enabled", () => {
it("caches partial data", () => {
service.cacheData({ token: "123456" });
expect(mockSignal.set).toHaveBeenCalledWith({
@@ -126,46 +88,18 @@ describe("TwoFactorAuthComponentCacheService", () => {
selectedProviderType: undefined,
});
});
it("does not cache data when feature is disabled", async () => {
mockConfigService.getFeatureFlag.mockResolvedValue(false);
await service.init();
service.cacheData({ token: "123456" });
expect(mockSignal.set).not.toHaveBeenCalled();
});
});
describe("clearCachedData", () => {
beforeEach(async () => {
mockConfigService.getFeatureFlag.mockResolvedValue(true);
await service.init();
});
it("clears cached data when feature is enabled", () => {
it("clears cached data", () => {
service.clearCachedData();
expect(mockSignal.set).toHaveBeenCalledWith(null);
});
it("does not clear cached data when feature is disabled", async () => {
mockConfigService.getFeatureFlag.mockResolvedValue(false);
await service.init();
service.clearCachedData();
expect(mockSignal.set).not.toHaveBeenCalled();
});
});
describe("getCachedData", () => {
beforeEach(async () => {
mockConfigService.getFeatureFlag.mockResolvedValue(true);
await service.init();
});
it("returns cached data when feature is enabled", () => {
it("returns cached data", () => {
const testData = new TwoFactorAuthComponentCache();
testData.token = "123456";
testData.remember = true;
@@ -177,15 +111,5 @@ describe("TwoFactorAuthComponentCacheService", () => {
expect(result).toEqual(testData);
expect(mockSignal).toHaveBeenCalled();
});
it("returns null when feature is disabled", async () => {
mockConfigService.getFeatureFlag.mockResolvedValue(false);
await service.init();
const result = service.getCachedData();
expect(result).toBeNull();
expect(mockSignal).not.toHaveBeenCalled();
});
});
});

View File

@@ -3,8 +3,6 @@ import { Jsonify } from "type-fest";
import { ViewCacheService } from "@bitwarden/angular/platform/view-cache";
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_AUTH_COMPONENT_CACHE_KEY = "two-factor-auth-component-cache";
@@ -40,10 +38,6 @@ export interface TwoFactorAuthComponentData {
@Injectable()
export class TwoFactorAuthComponentCacheService {
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 TwoFactorAuthData.
@@ -57,23 +51,10 @@ export class TwoFactorAuthComponentCacheService {
constructor() {}
/**
* Must be called once before interacting with the cached data.
*/
async init() {
this.featureEnabled = await this.configService.getFeatureFlag(
FeatureFlag.PM9115_TwoFactorExtensionDataPersistence,
);
}
/**
* Update the cache with the new TwoFactorAuthData.
*/
cacheData(data: TwoFactorAuthComponentData): void {
if (!this.featureEnabled) {
return;
}
this.twoFactorAuthComponentCache.set({
token: data.token,
remember: data.remember,
@@ -85,10 +66,6 @@ export class TwoFactorAuthComponentCacheService {
* Clears the cached TwoFactorAuthData.
*/
clearCachedData(): void {
if (!this.featureEnabled) {
return;
}
this.twoFactorAuthComponentCache.set(null);
}
@@ -96,10 +73,6 @@ export class TwoFactorAuthComponentCacheService {
* Returns the cached TwoFactorAuthData (when available).
*/
getCachedData(): TwoFactorAuthComponentCache | null {
if (!this.featureEnabled) {
return null;
}
return this.twoFactorAuthComponentCache();
}
}

View File

@@ -121,7 +121,6 @@ describe("TwoFactorAuthComponent", () => {
mockTwoFactorAuthCompCacheService = mock<TwoFactorAuthComponentCacheService>();
mockTwoFactorAuthCompCacheService.getCachedData.mockReturnValue(null);
mockTwoFactorAuthCompCacheService.init.mockResolvedValue();
mockUserDecryptionOpts = {
noMasterPassword: new UserDecryptionOptions({

View File

@@ -60,11 +60,11 @@ import {
TwoFactorAuthDuoIcon,
} from "../icons/two-factor-auth";
import { TwoFactorAuthAuthenticatorComponent } from "./child-components/two-factor-auth-authenticator.component";
import { TwoFactorAuthAuthenticatorComponent } from "./child-components/two-factor-auth-authenticator/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";
import { TwoFactorAuthWebAuthnComponent } from "./child-components/two-factor-auth-webauthn/two-factor-auth-webauthn.component";
import { TwoFactorAuthYubikeyComponent } from "./child-components/two-factor-auth-yubikey.component";
import { TwoFactorAuthYubikeyComponent } from "./child-components/two-factor-auth-yubikey/two-factor-auth-yubikey.component";
import {
TwoFactorAuthComponentCacheService,
TwoFactorAuthComponentData,
@@ -180,9 +180,6 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy {
this.listenForAuthnSessionTimeout();
// Initialize the cache
await this.twoFactorAuthComponentCacheService.init();
// Load cached form data if available
let loadedCachedProviderType = false;
const cachedData = this.twoFactorAuthComponentCacheService.getCachedData();