mirror of
https://github.com/bitwarden/browser
synced 2026-02-23 16:13:21 +00:00
Merge branch 'main' into auth/pm-26209/bugfix-desktop-error-on-auth-request-approval
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { ComponentFixture, fakeAsync, TestBed, tick } from "@angular/core/testing";
|
||||
import { By } from "@angular/platform-browser";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { mock } from "jest-mock-extended";
|
||||
@@ -37,7 +37,12 @@ import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { newGuid } from "@bitwarden/guid";
|
||||
import { BiometricStateService, BiometricsService, KeyService } from "@bitwarden/key-management";
|
||||
import {
|
||||
BiometricStateService,
|
||||
BiometricsService,
|
||||
BiometricsStatus,
|
||||
KeyService,
|
||||
} from "@bitwarden/key-management";
|
||||
|
||||
import { BrowserApi } from "../../../platform/browser/browser-api";
|
||||
import BrowserPopupUtils from "../../../platform/browser/browser-popup-utils";
|
||||
@@ -64,6 +69,7 @@ describe("AccountSecurityComponent", () => {
|
||||
const apiService = mock<ApiService>();
|
||||
const billingService = mock<BillingAccountProfileStateService>();
|
||||
const biometricStateService = mock<BiometricStateService>();
|
||||
const biometricsService = mock<BiometricsService>();
|
||||
const configService = mock<ConfigService>();
|
||||
const dialogService = mock<DialogService>();
|
||||
const keyService = mock<KeyService>();
|
||||
@@ -75,6 +81,7 @@ describe("AccountSecurityComponent", () => {
|
||||
const validationService = mock<ValidationService>();
|
||||
const vaultNudgesService = mock<NudgesService>();
|
||||
const vaultTimeoutSettingsService = mock<VaultTimeoutSettingsService>();
|
||||
const mockI18nService = mock<I18nService>();
|
||||
|
||||
// Mock subjects to control the phishing detection observables
|
||||
let phishingAvailableSubject: BehaviorSubject<boolean>;
|
||||
@@ -91,14 +98,14 @@ describe("AccountSecurityComponent", () => {
|
||||
provide: BillingAccountProfileStateService,
|
||||
useValue: billingService,
|
||||
},
|
||||
{ provide: BiometricsService, useValue: mock<BiometricsService>() },
|
||||
{ provide: BiometricsService, useValue: biometricsService },
|
||||
{ provide: BiometricStateService, useValue: biometricStateService },
|
||||
{ provide: CipherService, useValue: mock<CipherService>() },
|
||||
{ provide: CollectionService, useValue: mock<CollectionService>() },
|
||||
{ provide: ConfigService, useValue: configService },
|
||||
{ provide: DialogService, useValue: dialogService },
|
||||
{ provide: EnvironmentService, useValue: mock<EnvironmentService>() },
|
||||
{ provide: I18nService, useValue: mock<I18nService>() },
|
||||
{ provide: I18nService, useValue: mockI18nService },
|
||||
{ provide: KeyService, useValue: keyService },
|
||||
{ provide: LockService, useValue: lockService },
|
||||
{ provide: LogService, useValue: mock<LogService>() },
|
||||
@@ -153,6 +160,7 @@ describe("AccountSecurityComponent", () => {
|
||||
pinServiceAbstraction.isPinSet.mockResolvedValue(false);
|
||||
configService.getFeatureFlag$.mockReturnValue(of(false));
|
||||
billingService.hasPremiumPersonally$.mockReturnValue(of(true));
|
||||
mockI18nService.t.mockImplementation((key) => `${key}-used-i18n`);
|
||||
|
||||
policyService.policiesByType$.mockReturnValue(of([null]));
|
||||
|
||||
@@ -459,4 +467,118 @@ describe("AccountSecurityComponent", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("biometrics polling timer", () => {
|
||||
let browserApiSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
browserApiSpy = jest.spyOn(BrowserApi, "permissionsGranted");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
component.ngOnDestroy();
|
||||
});
|
||||
|
||||
it("disables biometric control when canEnableBiometricUnlock is false", fakeAsync(async () => {
|
||||
biometricsService.canEnableBiometricUnlock.mockResolvedValue(false);
|
||||
|
||||
await component.ngOnInit();
|
||||
tick();
|
||||
|
||||
expect(component.form.controls.biometric.disabled).toBe(true);
|
||||
}));
|
||||
|
||||
it("enables biometric control when canEnableBiometricUnlock is true", fakeAsync(async () => {
|
||||
biometricsService.canEnableBiometricUnlock.mockResolvedValue(true);
|
||||
|
||||
await component.ngOnInit();
|
||||
tick();
|
||||
|
||||
expect(component.form.controls.biometric.disabled).toBe(false);
|
||||
}));
|
||||
|
||||
it("skips status check when nativeMessaging permission is not granted and not Safari", fakeAsync(async () => {
|
||||
biometricsService.canEnableBiometricUnlock.mockResolvedValue(true);
|
||||
browserApiSpy.mockResolvedValue(false);
|
||||
platformUtilsService.isSafari.mockReturnValue(false);
|
||||
|
||||
await component.ngOnInit();
|
||||
tick();
|
||||
|
||||
expect(biometricsService.getBiometricsStatusForUser).not.toHaveBeenCalled();
|
||||
expect(component.biometricUnavailabilityReason).toBeUndefined();
|
||||
}));
|
||||
|
||||
it("checks biometrics status when nativeMessaging permission is granted", fakeAsync(async () => {
|
||||
biometricsService.canEnableBiometricUnlock.mockResolvedValue(true);
|
||||
browserApiSpy.mockResolvedValue(true);
|
||||
platformUtilsService.isSafari.mockReturnValue(false);
|
||||
biometricsService.getBiometricsStatusForUser.mockResolvedValue(
|
||||
BiometricsStatus.DesktopDisconnected,
|
||||
);
|
||||
|
||||
await component.ngOnInit();
|
||||
tick();
|
||||
|
||||
expect(biometricsService.getBiometricsStatusForUser).toHaveBeenCalledWith(mockUserId);
|
||||
}));
|
||||
|
||||
it("should check status on Safari", fakeAsync(async () => {
|
||||
biometricsService.canEnableBiometricUnlock.mockResolvedValue(true);
|
||||
browserApiSpy.mockResolvedValue(false);
|
||||
platformUtilsService.isSafari.mockReturnValue(true);
|
||||
biometricsService.getBiometricsStatusForUser.mockResolvedValue(
|
||||
BiometricsStatus.DesktopDisconnected,
|
||||
);
|
||||
|
||||
await component.ngOnInit();
|
||||
tick();
|
||||
|
||||
expect(biometricsService.getBiometricsStatusForUser).toHaveBeenCalledWith(mockUserId);
|
||||
}));
|
||||
|
||||
test.each([
|
||||
[
|
||||
BiometricsStatus.DesktopDisconnected,
|
||||
"biometricsStatusHelptextDesktopDisconnected-used-i18n",
|
||||
],
|
||||
[
|
||||
BiometricsStatus.NotEnabledInConnectedDesktopApp,
|
||||
"biometricsStatusHelptextNotEnabledInDesktop-used-i18n",
|
||||
],
|
||||
[
|
||||
BiometricsStatus.HardwareUnavailable,
|
||||
"biometricsStatusHelptextHardwareUnavailable-used-i18n",
|
||||
],
|
||||
])(
|
||||
"sets expected unavailability reason for %s status when biometric not available",
|
||||
fakeAsync(async (biometricStatus: BiometricsStatus, expected: string) => {
|
||||
biometricsService.canEnableBiometricUnlock.mockResolvedValue(false);
|
||||
browserApiSpy.mockResolvedValue(true);
|
||||
platformUtilsService.isSafari.mockReturnValue(false);
|
||||
biometricsService.getBiometricsStatusForUser.mockResolvedValue(biometricStatus);
|
||||
|
||||
await component.ngOnInit();
|
||||
tick();
|
||||
|
||||
expect(component.biometricUnavailabilityReason).toBe(expected);
|
||||
}),
|
||||
);
|
||||
|
||||
it("should not set unavailability reason for error statuses when biometric is available", fakeAsync(async () => {
|
||||
biometricsService.canEnableBiometricUnlock.mockResolvedValue(true);
|
||||
browserApiSpy.mockResolvedValue(true);
|
||||
platformUtilsService.isSafari.mockReturnValue(false);
|
||||
biometricsService.getBiometricsStatusForUser.mockResolvedValue(
|
||||
BiometricsStatus.DesktopDisconnected,
|
||||
);
|
||||
|
||||
await component.ngOnInit();
|
||||
tick();
|
||||
|
||||
// Status is DesktopDisconnected but biometric IS available, so don't show error
|
||||
expect(component.biometricUnavailabilityReason).toBe("");
|
||||
component.ngOnDestroy();
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -149,6 +149,7 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
|
||||
|
||||
protected refreshTimeoutSettings$ = new BehaviorSubject<void>(undefined);
|
||||
private destroy$ = new Subject<void>();
|
||||
private readonly BIOMETRICS_POLLING_INTERVAL = 2000;
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
@@ -264,10 +265,9 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
|
||||
};
|
||||
this.form.patchValue(initialValues, { emitEvent: false });
|
||||
|
||||
timer(0, 1000)
|
||||
timer(0, this.BIOMETRICS_POLLING_INTERVAL)
|
||||
.pipe(
|
||||
switchMap(async () => {
|
||||
const status = await this.biometricsService.getBiometricsStatusForUser(activeAccount.id);
|
||||
const biometricSettingAvailable = await this.biometricsService.canEnableBiometricUnlock();
|
||||
if (!biometricSettingAvailable) {
|
||||
this.form.controls.biometric.disable({ emitEvent: false });
|
||||
@@ -275,6 +275,15 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
|
||||
this.form.controls.biometric.enable({ emitEvent: false });
|
||||
}
|
||||
|
||||
// Biometrics status shouldn't be checked if permissions are needed.
|
||||
const needsPermissionPrompt =
|
||||
!(await BrowserApi.permissionsGranted(["nativeMessaging"])) &&
|
||||
!this.platformUtilsService.isSafari();
|
||||
if (needsPermissionPrompt) {
|
||||
return;
|
||||
}
|
||||
|
||||
const status = await this.biometricsService.getBiometricsStatusForUser(activeAccount.id);
|
||||
if (status === BiometricsStatus.DesktopDisconnected && !biometricSettingAvailable) {
|
||||
this.biometricUnavailabilityReason = this.i18nService.t(
|
||||
"biometricsStatusHelptextDesktopDisconnected",
|
||||
|
||||
@@ -125,6 +125,7 @@ import { FileUploadService as FileUploadServiceAbstraction } from "@bitwarden/co
|
||||
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService as LogServiceAbstraction } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { RegisterSdkService } from "@bitwarden/common/platform/abstractions/sdk/register-sdk.service";
|
||||
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
|
||||
import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
|
||||
import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
@@ -163,6 +164,7 @@ import { MigrationRunner } from "@bitwarden/common/platform/services/migration-r
|
||||
import { DefaultSdkClientFactory } from "@bitwarden/common/platform/services/sdk/default-sdk-client-factory";
|
||||
import { DefaultSdkService } from "@bitwarden/common/platform/services/sdk/default-sdk.service";
|
||||
import { NoopSdkClientFactory } from "@bitwarden/common/platform/services/sdk/noop-sdk-client-factory";
|
||||
import { DefaultRegisterSdkService } from "@bitwarden/common/platform/services/sdk/register-sdk.service";
|
||||
import { SystemService } from "@bitwarden/common/platform/services/system.service";
|
||||
import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service";
|
||||
import { PrimarySecondaryStorageService } from "@bitwarden/common/platform/storage/primary-secondary-storage.service";
|
||||
@@ -463,6 +465,7 @@ export default class MainBackground {
|
||||
themeStateService: DefaultThemeStateService;
|
||||
autoSubmitLoginBackground: AutoSubmitLoginBackground;
|
||||
sdkService: SdkService;
|
||||
registerSdkService: RegisterSdkService;
|
||||
sdkLoadService: SdkLoadService;
|
||||
cipherAuthorizationService: CipherAuthorizationService;
|
||||
endUserNotificationService: EndUserNotificationService;
|
||||
@@ -578,7 +581,7 @@ export default class MainBackground {
|
||||
"ephemeral",
|
||||
"bitwarden-ephemeral",
|
||||
);
|
||||
await sessionStorage.save("session-key", derivedKey);
|
||||
await sessionStorage.save("session-key", derivedKey.toJSON());
|
||||
return derivedKey;
|
||||
});
|
||||
|
||||
@@ -797,18 +800,6 @@ export default class MainBackground {
|
||||
this.apiService,
|
||||
this.accountService,
|
||||
);
|
||||
this.keyConnectorService = new KeyConnectorService(
|
||||
this.accountService,
|
||||
this.masterPasswordService,
|
||||
this.keyService,
|
||||
this.apiService,
|
||||
this.tokenService,
|
||||
this.logService,
|
||||
this.organizationService,
|
||||
this.keyGenerationService,
|
||||
logoutCallback,
|
||||
this.stateProvider,
|
||||
);
|
||||
|
||||
this.authService = new AuthService(
|
||||
this.accountService,
|
||||
@@ -846,6 +837,37 @@ export default class MainBackground {
|
||||
this.configService,
|
||||
);
|
||||
|
||||
this.registerSdkService = new DefaultRegisterSdkService(
|
||||
sdkClientFactory,
|
||||
this.environmentService,
|
||||
this.platformUtilsService,
|
||||
this.accountService,
|
||||
this.apiService,
|
||||
this.stateProvider,
|
||||
this.configService,
|
||||
);
|
||||
|
||||
this.accountCryptographicStateService = new DefaultAccountCryptographicStateService(
|
||||
this.stateProvider,
|
||||
);
|
||||
|
||||
this.keyConnectorService = new KeyConnectorService(
|
||||
this.accountService,
|
||||
this.masterPasswordService,
|
||||
this.keyService,
|
||||
this.apiService,
|
||||
this.tokenService,
|
||||
this.logService,
|
||||
this.organizationService,
|
||||
this.keyGenerationService,
|
||||
logoutCallback,
|
||||
this.stateProvider,
|
||||
this.configService,
|
||||
this.registerSdkService,
|
||||
this.securityStateService,
|
||||
this.accountCryptographicStateService,
|
||||
);
|
||||
|
||||
this.pinService = new PinService(
|
||||
this.encryptService,
|
||||
this.logService,
|
||||
@@ -1013,9 +1035,7 @@ export default class MainBackground {
|
||||
this.avatarService = new AvatarService(this.apiService, this.stateProvider);
|
||||
|
||||
this.providerService = new ProviderService(this.stateProvider);
|
||||
this.accountCryptographicStateService = new DefaultAccountCryptographicStateService(
|
||||
this.stateProvider,
|
||||
);
|
||||
|
||||
this.syncService = new DefaultSyncService(
|
||||
this.masterPasswordService,
|
||||
this.accountService,
|
||||
|
||||
@@ -104,6 +104,7 @@ import {
|
||||
EnvironmentService,
|
||||
RegionConfig,
|
||||
} from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { RegisterSdkService } from "@bitwarden/common/platform/abstractions/sdk/register-sdk.service";
|
||||
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
|
||||
import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
|
||||
import { LogLevelType } from "@bitwarden/common/platform/enums";
|
||||
@@ -124,6 +125,7 @@ import { MigrationRunner } from "@bitwarden/common/platform/services/migration-r
|
||||
import { DefaultSdkClientFactory } from "@bitwarden/common/platform/services/sdk/default-sdk-client-factory";
|
||||
import { DefaultSdkService } from "@bitwarden/common/platform/services/sdk/default-sdk.service";
|
||||
import { NoopSdkClientFactory } from "@bitwarden/common/platform/services/sdk/noop-sdk-client-factory";
|
||||
import { DefaultRegisterSdkService } from "@bitwarden/common/platform/services/sdk/register-sdk.service";
|
||||
import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider";
|
||||
import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
@@ -323,6 +325,7 @@ export class ServiceContainer {
|
||||
kdfConfigService: KdfConfigService;
|
||||
taskSchedulerService: TaskSchedulerService;
|
||||
sdkService: SdkService;
|
||||
registerSdkService: RegisterSdkService;
|
||||
sdkLoadService: SdkLoadService;
|
||||
cipherAuthorizationService: CipherAuthorizationService;
|
||||
ssoUrlService: SsoUrlService;
|
||||
@@ -632,26 +635,10 @@ export class ServiceContainer {
|
||||
this.accountService,
|
||||
);
|
||||
|
||||
this.keyConnectorService = new KeyConnectorService(
|
||||
this.accountService,
|
||||
this.masterPasswordService,
|
||||
this.keyService,
|
||||
this.apiService,
|
||||
this.tokenService,
|
||||
this.logService,
|
||||
this.organizationService,
|
||||
this.keyGenerationService,
|
||||
logoutCallback,
|
||||
this.accountCryptographicStateService = new DefaultAccountCryptographicStateService(
|
||||
this.stateProvider,
|
||||
);
|
||||
|
||||
this.twoFactorService = new DefaultTwoFactorService(
|
||||
this.i18nService,
|
||||
this.platformUtilsService,
|
||||
this.globalStateProvider,
|
||||
this.twoFactorApiService,
|
||||
);
|
||||
|
||||
const sdkClientFactory = flagEnabled("sdk")
|
||||
? new DefaultSdkClientFactory()
|
||||
: new NoopSdkClientFactory();
|
||||
@@ -670,6 +657,41 @@ export class ServiceContainer {
|
||||
customUserAgent,
|
||||
);
|
||||
|
||||
this.registerSdkService = new DefaultRegisterSdkService(
|
||||
sdkClientFactory,
|
||||
this.environmentService,
|
||||
this.platformUtilsService,
|
||||
this.accountService,
|
||||
this.apiService,
|
||||
this.stateProvider,
|
||||
this.configService,
|
||||
customUserAgent,
|
||||
);
|
||||
|
||||
this.keyConnectorService = new KeyConnectorService(
|
||||
this.accountService,
|
||||
this.masterPasswordService,
|
||||
this.keyService,
|
||||
this.apiService,
|
||||
this.tokenService,
|
||||
this.logService,
|
||||
this.organizationService,
|
||||
this.keyGenerationService,
|
||||
logoutCallback,
|
||||
this.stateProvider,
|
||||
this.configService,
|
||||
this.registerSdkService,
|
||||
this.securityStateService,
|
||||
this.accountCryptographicStateService,
|
||||
);
|
||||
|
||||
this.twoFactorService = new DefaultTwoFactorService(
|
||||
this.i18nService,
|
||||
this.platformUtilsService,
|
||||
this.globalStateProvider,
|
||||
this.twoFactorApiService,
|
||||
);
|
||||
|
||||
this.passwordStrengthService = new PasswordStrengthService();
|
||||
|
||||
this.passwordGenerationService = legacyPasswordGenerationServiceFactory(
|
||||
@@ -719,10 +741,6 @@ export class ServiceContainer {
|
||||
this.accountService,
|
||||
);
|
||||
|
||||
this.accountCryptographicStateService = new DefaultAccountCryptographicStateService(
|
||||
this.stateProvider,
|
||||
);
|
||||
|
||||
this.loginStrategyService = new LoginStrategyService(
|
||||
this.accountService,
|
||||
this.masterPasswordService,
|
||||
|
||||
Reference in New Issue
Block a user