mirror of
https://github.com/bitwarden/browser.git
synced 2025-12-05 09:14:28 +01:00
Merge a6b2435751 into d32365fbba
This commit is contained in:
commit
99df641f34
@ -16,6 +16,7 @@
|
||||
[disabled]="unlockingViaBiometrics || !biometricsAvailable"
|
||||
[loading]="unlockingViaBiometrics"
|
||||
block
|
||||
[bitTooltip]="biometricUnavailabilityReason"
|
||||
(click)="unlockViaBiometrics()"
|
||||
>
|
||||
<span> {{ biometricUnlockBtnText | i18n }}</span>
|
||||
|
||||
@ -11,7 +11,7 @@ import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { LogoutService } from "@bitwarden/auth/common";
|
||||
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
@ -57,6 +57,7 @@ import {
|
||||
import {
|
||||
LockComponentService,
|
||||
UnlockOption,
|
||||
UnlockOptionValue,
|
||||
UnlockOptions,
|
||||
} from "../services/lock-component.service";
|
||||
|
||||
@ -808,4 +809,250 @@ describe("LockComponent", () => {
|
||||
expect(mockRouter.navigate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("setDefaultActiveUnlockOption", () => {
|
||||
it.each([
|
||||
[
|
||||
"biometrics enabled",
|
||||
{
|
||||
biometrics: { enabled: true, biometricsStatus: BiometricsStatus.Available },
|
||||
pin: { enabled: false },
|
||||
masterPassword: { enabled: false },
|
||||
} as UnlockOptions,
|
||||
UnlockOption.Biometrics,
|
||||
],
|
||||
[
|
||||
"biometrics disabled, pin enabled",
|
||||
{
|
||||
biometrics: { enabled: false, biometricsStatus: BiometricsStatus.NotEnabledLocally },
|
||||
pin: { enabled: true },
|
||||
masterPassword: { enabled: false },
|
||||
} as UnlockOptions,
|
||||
UnlockOption.Pin,
|
||||
],
|
||||
[
|
||||
"biometrics and pin disabled, masterPassword enabled",
|
||||
{
|
||||
biometrics: { enabled: false, biometricsStatus: BiometricsStatus.NotEnabledLocally },
|
||||
pin: { enabled: false },
|
||||
masterPassword: { enabled: true },
|
||||
} as UnlockOptions,
|
||||
UnlockOption.MasterPassword,
|
||||
],
|
||||
[
|
||||
"hardware unavailable, no other options",
|
||||
{
|
||||
biometrics: { enabled: false, biometricsStatus: BiometricsStatus.HardwareUnavailable },
|
||||
pin: { enabled: false },
|
||||
masterPassword: { enabled: false },
|
||||
} as UnlockOptions,
|
||||
UnlockOption.Biometrics,
|
||||
],
|
||||
[
|
||||
"desktop disconnected, no other options",
|
||||
{
|
||||
biometrics: { enabled: false, biometricsStatus: BiometricsStatus.DesktopDisconnected },
|
||||
pin: { enabled: false },
|
||||
masterPassword: { enabled: false },
|
||||
} as UnlockOptions,
|
||||
UnlockOption.Biometrics,
|
||||
],
|
||||
[
|
||||
"not enabled in connected desktop app, no other options",
|
||||
{
|
||||
biometrics: {
|
||||
enabled: false,
|
||||
biometricsStatus: BiometricsStatus.NotEnabledInConnectedDesktopApp,
|
||||
},
|
||||
pin: { enabled: false },
|
||||
masterPassword: { enabled: false },
|
||||
} as UnlockOptions,
|
||||
UnlockOption.Biometrics,
|
||||
],
|
||||
[
|
||||
"biometrics over pin priority",
|
||||
{
|
||||
biometrics: { enabled: true, biometricsStatus: BiometricsStatus.Available },
|
||||
pin: { enabled: true },
|
||||
masterPassword: { enabled: false },
|
||||
} as UnlockOptions,
|
||||
UnlockOption.Biometrics,
|
||||
],
|
||||
[
|
||||
"biometrics over masterPassword priority",
|
||||
{
|
||||
biometrics: { enabled: true, biometricsStatus: BiometricsStatus.Available },
|
||||
pin: { enabled: false },
|
||||
masterPassword: { enabled: true },
|
||||
} as UnlockOptions,
|
||||
UnlockOption.Biometrics,
|
||||
],
|
||||
[
|
||||
"pin over masterPassword priority",
|
||||
{
|
||||
biometrics: { enabled: false, biometricsStatus: BiometricsStatus.NotEnabledLocally },
|
||||
pin: { enabled: true },
|
||||
masterPassword: { enabled: true },
|
||||
} as UnlockOptions,
|
||||
UnlockOption.Pin,
|
||||
],
|
||||
[
|
||||
"all options enabled",
|
||||
{
|
||||
biometrics: { enabled: true, biometricsStatus: BiometricsStatus.Available },
|
||||
pin: { enabled: true },
|
||||
masterPassword: { enabled: true },
|
||||
} as UnlockOptions,
|
||||
UnlockOption.Biometrics,
|
||||
],
|
||||
])(
|
||||
"should set active unlock option to $1 when %s",
|
||||
async (
|
||||
description: string,
|
||||
unlockOptions: UnlockOptions,
|
||||
expectedOption: UnlockOptionValue,
|
||||
) => {
|
||||
await component["setDefaultActiveUnlockOption"](unlockOptions);
|
||||
|
||||
expect(component.activeUnlockOption).toBe(expectedOption);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("handleActiveAccountChange", () => {
|
||||
const mockActiveAccount: Account = {
|
||||
id: userId,
|
||||
email: "test@example.com",
|
||||
name: "Test User",
|
||||
} as Account;
|
||||
|
||||
beforeEach(async () => {
|
||||
component.activeAccount = mockActiveAccount;
|
||||
});
|
||||
|
||||
it("should return early when account already has user key", async () => {
|
||||
mockKeyService.hasUserKey.mockResolvedValue(true);
|
||||
|
||||
await component["handleActiveAccountChange"](mockActiveAccount);
|
||||
|
||||
expect(mockKeyService.hasUserKey).toHaveBeenCalledWith(userId);
|
||||
expect(mockAnonLayoutWrapperDataService.setAnonLayoutWrapperData).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should set email as page subtitle when account is unlocked", async () => {
|
||||
mockKeyService.hasUserKey.mockResolvedValue(false);
|
||||
mockLockComponentService.getAvailableUnlockOptions$.mockReturnValue(
|
||||
of({
|
||||
biometrics: { enabled: true, biometricsStatus: BiometricsStatus.Available },
|
||||
pin: { enabled: false },
|
||||
masterPassword: { enabled: false },
|
||||
} as UnlockOptions),
|
||||
);
|
||||
mockBiometricService.getBiometricsStatusForUser.mockResolvedValue(BiometricsStatus.Available);
|
||||
|
||||
await component["handleActiveAccountChange"](mockActiveAccount);
|
||||
|
||||
expect(mockAnonLayoutWrapperDataService.setAnonLayoutWrapperData).toHaveBeenCalledWith({
|
||||
pageSubtitle: mockActiveAccount.email,
|
||||
});
|
||||
});
|
||||
|
||||
it("should logout user when no unlock options are available", async () => {
|
||||
mockKeyService.hasUserKey.mockResolvedValue(false);
|
||||
mockLockComponentService.getAvailableUnlockOptions$.mockReturnValue(
|
||||
of({
|
||||
biometrics: { enabled: false, biometricsStatus: BiometricsStatus.UnlockNeeded },
|
||||
pin: { enabled: false },
|
||||
masterPassword: { enabled: false },
|
||||
} as UnlockOptions),
|
||||
);
|
||||
mockBiometricService.getBiometricsStatusForUser.mockResolvedValue(
|
||||
BiometricsStatus.UnlockNeeded,
|
||||
);
|
||||
|
||||
await component["handleActiveAccountChange"](mockActiveAccount);
|
||||
|
||||
expect(mockLogService.warning).toHaveBeenCalledWith(
|
||||
"[LockComponent] User cannot unlock again. Logging out!",
|
||||
);
|
||||
expect(mockLogoutService.logout).toHaveBeenCalledWith(userId);
|
||||
});
|
||||
|
||||
it("should not logout when master password is enabled", async () => {
|
||||
mockKeyService.hasUserKey.mockResolvedValue(false);
|
||||
mockLockComponentService.getAvailableUnlockOptions$.mockReturnValue(
|
||||
of({
|
||||
biometrics: { enabled: false, biometricsStatus: BiometricsStatus.UnlockNeeded },
|
||||
pin: { enabled: false },
|
||||
masterPassword: { enabled: true },
|
||||
} as UnlockOptions),
|
||||
);
|
||||
mockBiometricService.getBiometricsStatusForUser.mockResolvedValue(
|
||||
BiometricsStatus.UnlockNeeded,
|
||||
);
|
||||
|
||||
await component["handleActiveAccountChange"](mockActiveAccount);
|
||||
|
||||
expect(mockLogoutService.logout).not.toHaveBeenCalled();
|
||||
expect(component.activeUnlockOption).toBe(UnlockOption.MasterPassword);
|
||||
});
|
||||
|
||||
it("should not logout when pin is enabled", async () => {
|
||||
mockKeyService.hasUserKey.mockResolvedValue(false);
|
||||
mockLockComponentService.getAvailableUnlockOptions$.mockReturnValue(
|
||||
of({
|
||||
biometrics: { enabled: false, biometricsStatus: BiometricsStatus.UnlockNeeded },
|
||||
pin: { enabled: true },
|
||||
masterPassword: { enabled: false },
|
||||
} as UnlockOptions),
|
||||
);
|
||||
mockBiometricService.getBiometricsStatusForUser.mockResolvedValue(
|
||||
BiometricsStatus.UnlockNeeded,
|
||||
);
|
||||
|
||||
await component["handleActiveAccountChange"](mockActiveAccount);
|
||||
|
||||
expect(mockLogoutService.logout).not.toHaveBeenCalled();
|
||||
expect(component.activeUnlockOption).toBe(UnlockOption.Pin);
|
||||
});
|
||||
|
||||
it("should not logout when biometrics is available", async () => {
|
||||
mockKeyService.hasUserKey.mockResolvedValue(false);
|
||||
mockLockComponentService.getAvailableUnlockOptions$.mockReturnValue(
|
||||
of({
|
||||
biometrics: { enabled: true, biometricsStatus: BiometricsStatus.Available },
|
||||
pin: { enabled: false },
|
||||
masterPassword: { enabled: false },
|
||||
} as UnlockOptions),
|
||||
);
|
||||
mockBiometricService.getBiometricsStatusForUser.mockResolvedValue(BiometricsStatus.Available);
|
||||
|
||||
await component["handleActiveAccountChange"](mockActiveAccount);
|
||||
|
||||
expect(mockLogoutService.logout).not.toHaveBeenCalled();
|
||||
expect(component.activeUnlockOption).toBe(UnlockOption.Biometrics);
|
||||
});
|
||||
|
||||
it("should not logout when biometrics is temporarily unavailable but no other options", async () => {
|
||||
mockKeyService.hasUserKey.mockResolvedValue(false);
|
||||
mockLockComponentService.getAvailableUnlockOptions$.mockReturnValue(
|
||||
of({
|
||||
biometrics: {
|
||||
enabled: false,
|
||||
biometricsStatus: BiometricsStatus.HardwareUnavailable,
|
||||
},
|
||||
pin: { enabled: false },
|
||||
masterPassword: { enabled: false },
|
||||
} as UnlockOptions),
|
||||
);
|
||||
mockBiometricService.getBiometricsStatusForUser.mockResolvedValue(
|
||||
BiometricsStatus.HardwareUnavailable,
|
||||
);
|
||||
|
||||
await component["handleActiveAccountChange"](mockActiveAccount);
|
||||
|
||||
expect(mockLogoutService.logout).not.toHaveBeenCalled();
|
||||
expect(component.activeUnlockOption).toBe(UnlockOption.Biometrics);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -44,6 +44,7 @@ import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||
import { UserKey } from "@bitwarden/common/types/key";
|
||||
import {
|
||||
TooltipDirective,
|
||||
AsyncActionsModule,
|
||||
AnonLayoutWrapperDataService,
|
||||
ButtonModule,
|
||||
@ -87,6 +88,12 @@ type AfterUnlockActions = {
|
||||
/// Fixes safari autoprompt behavior
|
||||
const AUTOPROMPT_BIOMETRICS_PROCESS_RELOAD_DELAY = 5000;
|
||||
|
||||
const BIOMETRIC_UNLOCK_TEMPORARY_UNAVAILABLE_STATUSES = [
|
||||
BiometricsStatus.HardwareUnavailable,
|
||||
BiometricsStatus.DesktopDisconnected,
|
||||
BiometricsStatus.NotEnabledInConnectedDesktopApp,
|
||||
];
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
@ -101,6 +108,7 @@ const AUTOPROMPT_BIOMETRICS_PROCESS_RELOAD_DELAY = 5000;
|
||||
AsyncActionsModule,
|
||||
IconButtonModule,
|
||||
MasterPasswordLockComponent,
|
||||
TooltipDirective,
|
||||
],
|
||||
})
|
||||
export class LockComponent implements OnInit, OnDestroy {
|
||||
@ -279,7 +287,22 @@ export class LockComponent implements OnInit, OnDestroy {
|
||||
this.lockComponentService.getAvailableUnlockOptions$(activeAccount.id),
|
||||
);
|
||||
|
||||
this.setDefaultActiveUnlockOption(this.unlockOptions);
|
||||
const canUseBiometrics = [
|
||||
BiometricsStatus.Available,
|
||||
...BIOMETRIC_UNLOCK_TEMPORARY_UNAVAILABLE_STATUSES,
|
||||
].includes(await this.biometricService.getBiometricsStatusForUser(activeAccount.id));
|
||||
if (
|
||||
!this.unlockOptions?.masterPassword.enabled &&
|
||||
!this.unlockOptions?.pin.enabled &&
|
||||
!canUseBiometrics
|
||||
) {
|
||||
// User has no available unlock options, force logout. This happens for TDE users without a masterpassword, that don't have a persistent unlock method set.
|
||||
this.logService.warning("[LockComponent] User cannot unlock again. Logging out!");
|
||||
await this.logoutService.logout(activeAccount.id);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.setDefaultActiveUnlockOption(this.unlockOptions);
|
||||
|
||||
if (this.unlockOptions?.biometrics.enabled) {
|
||||
await this.handleBiometricsUnlockEnabled();
|
||||
@ -302,7 +325,7 @@ export class LockComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
private setDefaultActiveUnlockOption(unlockOptions: UnlockOptions | null) {
|
||||
private async setDefaultActiveUnlockOption(unlockOptions: UnlockOptions | null) {
|
||||
// Priorities should be Biometrics > Pin > Master Password for speed
|
||||
if (unlockOptions?.biometrics.enabled) {
|
||||
this.activeUnlockOption = UnlockOption.Biometrics;
|
||||
@ -310,6 +333,15 @@ export class LockComponent implements OnInit, OnDestroy {
|
||||
this.activeUnlockOption = UnlockOption.Pin;
|
||||
} else if (unlockOptions?.masterPassword.enabled) {
|
||||
this.activeUnlockOption = UnlockOption.MasterPassword;
|
||||
} else if (
|
||||
unlockOptions != null &&
|
||||
BIOMETRIC_UNLOCK_TEMPORARY_UNAVAILABLE_STATUSES.includes(
|
||||
unlockOptions.biometrics.biometricsStatus,
|
||||
)
|
||||
) {
|
||||
// If biometrics is temporarily unavailable for masterpassword-less users, but they have biometrics configured,
|
||||
// then show the biometrics screen so the user knows why they can't unlock, and to give them the option to log out.
|
||||
this.activeUnlockOption = UnlockOption.Biometrics;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user