[PM-5404, PM-3518] Migrate user decryption options to new service (#7344)
* create new user decryption options service * rename new service to user decryption options * add hasMasterPassword to user decryption options service * migrate device trust service to new user decryption options service * add migration for user-decryption-options * migrate sync service and calls to trust-device-service * rename abstraction file * migrate two factor component * migrate two factor spec * migrate sso component * migrate set-password component * migrate base login decryption component * migrate organization options component * fix component imports * add missing imports - remove state service calls - add update user decryption options method * remove acct decryption options from account * lint * fix tests and linting * fix browser * fix desktop * add user decryption options service to cli * remove default value from migration * bump migration number * fix merge conflict * fix vault timeout settings * fix cli * more fixes * add user decryption options service to deps of vault timeout settings service * update login strategy service with user decryption options * remove early return from sync bandaid for user decryption options * move user decryption options service to lib/auth * move user decryption options to libs/auth * fix reference * fix browser * check user decryption options after 2fa check * update migration and revert tsconfig changes * add more documentation * clear user decryption options on logout * fix tests by creating helper for user decryption options * fix tests * pr feedback * fix factory * update migration * add tests * update missed migration num in test
This commit is contained in:
parent
e2fe1e1567
commit
2111b37c32
|
@ -15,8 +15,8 @@ import {
|
|||
factory,
|
||||
} from "../../../platform/background/service-factories/factory-options";
|
||||
import {
|
||||
messagingServiceFactory,
|
||||
MessagingServiceInitOptions,
|
||||
messagingServiceFactory,
|
||||
} from "../../../platform/background/service-factories/messaging-service.factory";
|
||||
import {
|
||||
StateServiceInitOptions,
|
||||
|
|
|
@ -43,6 +43,11 @@ import {
|
|||
stateServiceFactory,
|
||||
} from "../../../platform/background/service-factories/state-service.factory";
|
||||
|
||||
import {
|
||||
UserDecryptionOptionsServiceInitOptions,
|
||||
userDecryptionOptionsServiceFactory,
|
||||
} from "./user-decryption-options-service.factory";
|
||||
|
||||
type DeviceTrustCryptoServiceFactoryOptions = FactoryOptions;
|
||||
|
||||
export type DeviceTrustCryptoServiceInitOptions = DeviceTrustCryptoServiceFactoryOptions &
|
||||
|
@ -54,7 +59,8 @@ export type DeviceTrustCryptoServiceInitOptions = DeviceTrustCryptoServiceFactor
|
|||
AppIdServiceInitOptions &
|
||||
DevicesApiServiceInitOptions &
|
||||
I18nServiceInitOptions &
|
||||
PlatformUtilsServiceInitOptions;
|
||||
PlatformUtilsServiceInitOptions &
|
||||
UserDecryptionOptionsServiceInitOptions;
|
||||
|
||||
export function deviceTrustCryptoServiceFactory(
|
||||
cache: { deviceTrustCryptoService?: DeviceTrustCryptoServiceAbstraction } & CachedServices,
|
||||
|
@ -75,6 +81,7 @@ export function deviceTrustCryptoServiceFactory(
|
|||
await devicesApiServiceFactory(cache, opts),
|
||||
await i18nServiceFactory(cache, opts),
|
||||
await platformUtilsServiceFactory(cache, opts),
|
||||
await userDecryptionOptionsServiceFactory(cache, opts),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -9,7 +9,10 @@ import {
|
|||
ApiServiceInitOptions,
|
||||
} from "../../../platform/background/service-factories/api-service.factory";
|
||||
import { appIdServiceFactory } from "../../../platform/background/service-factories/app-id-service.factory";
|
||||
import { billingAccountProfileStateServiceFactory } from "../../../platform/background/service-factories/billing-account-profile-state-service.factory";
|
||||
import {
|
||||
billingAccountProfileStateServiceFactory,
|
||||
BillingAccountProfileStateServiceInitOptions,
|
||||
} from "../../../platform/background/service-factories/billing-account-profile-state-service.factory";
|
||||
import {
|
||||
CryptoServiceInitOptions,
|
||||
cryptoServiceFactory,
|
||||
|
@ -70,6 +73,10 @@ import {
|
|||
} from "./key-connector-service.factory";
|
||||
import { tokenServiceFactory, TokenServiceInitOptions } from "./token-service.factory";
|
||||
import { twoFactorServiceFactory, TwoFactorServiceInitOptions } from "./two-factor-service.factory";
|
||||
import {
|
||||
internalUserDecryptionOptionServiceFactory,
|
||||
UserDecryptionOptionsServiceInitOptions,
|
||||
} from "./user-decryption-options-service.factory";
|
||||
|
||||
type LoginStrategyServiceFactoryOptions = FactoryOptions;
|
||||
|
||||
|
@ -90,7 +97,9 @@ export type LoginStrategyServiceInitOptions = LoginStrategyServiceFactoryOptions
|
|||
PasswordStrengthServiceInitOptions &
|
||||
DeviceTrustCryptoServiceInitOptions &
|
||||
AuthRequestServiceInitOptions &
|
||||
GlobalStateProviderInitOptions;
|
||||
UserDecryptionOptionsServiceInitOptions &
|
||||
GlobalStateProviderInitOptions &
|
||||
BillingAccountProfileStateServiceInitOptions;
|
||||
|
||||
export function loginStrategyServiceFactory(
|
||||
cache: { loginStrategyService?: LoginStrategyServiceAbstraction } & CachedServices,
|
||||
|
@ -119,6 +128,7 @@ export function loginStrategyServiceFactory(
|
|||
await policyServiceFactory(cache, opts),
|
||||
await deviceTrustCryptoServiceFactory(cache, opts),
|
||||
await authRequestServiceFactory(cache, opts),
|
||||
await internalUserDecryptionOptionServiceFactory(cache, opts),
|
||||
await globalStateProviderFactory(cache, opts),
|
||||
await billingAccountProfileStateServiceFactory(cache, opts),
|
||||
),
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
import {
|
||||
InternalUserDecryptionOptionsServiceAbstraction,
|
||||
UserDecryptionOptionsService,
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
|
||||
import {
|
||||
CachedServices,
|
||||
factory,
|
||||
FactoryOptions,
|
||||
} from "../../../platform/background/service-factories/factory-options";
|
||||
import {
|
||||
stateProviderFactory,
|
||||
StateProviderInitOptions,
|
||||
} from "../../../platform/background/service-factories/state-provider.factory";
|
||||
|
||||
type UserDecryptionOptionsServiceFactoryOptions = FactoryOptions;
|
||||
|
||||
export type UserDecryptionOptionsServiceInitOptions = UserDecryptionOptionsServiceFactoryOptions &
|
||||
StateProviderInitOptions;
|
||||
|
||||
export function userDecryptionOptionsServiceFactory(
|
||||
cache: {
|
||||
userDecryptionOptionsService?: InternalUserDecryptionOptionsServiceAbstraction;
|
||||
} & CachedServices,
|
||||
opts: UserDecryptionOptionsServiceInitOptions,
|
||||
): Promise<UserDecryptionOptionsServiceAbstraction> {
|
||||
return factory(
|
||||
cache,
|
||||
"userDecryptionOptionsService",
|
||||
opts,
|
||||
async () => new UserDecryptionOptionsService(await stateProviderFactory(cache, opts)),
|
||||
);
|
||||
}
|
||||
|
||||
export async function internalUserDecryptionOptionServiceFactory(
|
||||
cache: {
|
||||
userDecryptionOptionsService?: InternalUserDecryptionOptionsServiceAbstraction;
|
||||
} & CachedServices,
|
||||
opts: UserDecryptionOptionsServiceInitOptions,
|
||||
): Promise<InternalUserDecryptionOptionsServiceAbstraction> {
|
||||
return (await userDecryptionOptionsServiceFactory(
|
||||
cache,
|
||||
opts,
|
||||
)) as InternalUserDecryptionOptionsServiceAbstraction;
|
||||
}
|
|
@ -32,6 +32,10 @@ import {
|
|||
} from "../../../platform/background/service-factories/state-service.factory";
|
||||
|
||||
import { PinCryptoServiceInitOptions, pinCryptoServiceFactory } from "./pin-crypto-service.factory";
|
||||
import {
|
||||
userDecryptionOptionsServiceFactory,
|
||||
UserDecryptionOptionsServiceInitOptions,
|
||||
} from "./user-decryption-options-service.factory";
|
||||
import {
|
||||
UserVerificationApiServiceInitOptions,
|
||||
userVerificationApiServiceFactory,
|
||||
|
@ -44,6 +48,7 @@ export type UserVerificationServiceInitOptions = UserVerificationServiceFactoryO
|
|||
CryptoServiceInitOptions &
|
||||
I18nServiceInitOptions &
|
||||
UserVerificationApiServiceInitOptions &
|
||||
UserDecryptionOptionsServiceInitOptions &
|
||||
PinCryptoServiceInitOptions &
|
||||
LogServiceInitOptions &
|
||||
VaultTimeoutSettingsServiceInitOptions &
|
||||
|
@ -63,6 +68,7 @@ export function userVerificationServiceFactory(
|
|||
await cryptoServiceFactory(cache, opts),
|
||||
await i18nServiceFactory(cache, opts),
|
||||
await userVerificationApiServiceFactory(cache, opts),
|
||||
await userDecryptionOptionsServiceFactory(cache, opts),
|
||||
await pinCryptoServiceFactory(cache, opts),
|
||||
await logServiceFactory(cache, opts),
|
||||
await vaultTimeoutSettingsServiceFactory(cache, opts),
|
||||
|
|
|
@ -2,6 +2,7 @@ import { Component } from "@angular/core";
|
|||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
|
||||
import { SetPasswordComponent as BaseSetPasswordComponent } from "@bitwarden/angular/auth/components/set-password.component";
|
||||
import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
|
||||
|
@ -37,6 +38,7 @@ export class SetPasswordComponent extends BaseSetPasswordComponent {
|
|||
route: ActivatedRoute,
|
||||
organizationApiService: OrganizationApiServiceAbstraction,
|
||||
organizationUserService: OrganizationUserService,
|
||||
userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
|
||||
ssoLoginService: SsoLoginServiceAbstraction,
|
||||
dialogService: DialogService,
|
||||
) {
|
||||
|
@ -55,6 +57,7 @@ export class SetPasswordComponent extends BaseSetPasswordComponent {
|
|||
stateService,
|
||||
organizationApiService,
|
||||
organizationUserService,
|
||||
userDecryptionOptionsService,
|
||||
ssoLoginService,
|
||||
dialogService,
|
||||
);
|
||||
|
|
|
@ -3,7 +3,10 @@ import { ActivatedRoute, Router } from "@angular/router";
|
|||
|
||||
import { SsoComponent as BaseSsoComponent } from "@bitwarden/angular/auth/components/sso.component";
|
||||
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
|
||||
import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import {
|
||||
LoginStrategyServiceAbstraction,
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
|
@ -39,6 +42,7 @@ export class SsoComponent extends BaseSsoComponent {
|
|||
syncService: SyncService,
|
||||
environmentService: EnvironmentService,
|
||||
logService: LogService,
|
||||
userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
||||
configService: ConfigServiceAbstraction,
|
||||
protected authService: AuthService,
|
||||
@Inject(WINDOW) private win: Window,
|
||||
|
@ -56,6 +60,7 @@ export class SsoComponent extends BaseSsoComponent {
|
|||
environmentService,
|
||||
passwordGenerationService,
|
||||
logService,
|
||||
userDecryptionOptionsService,
|
||||
configService,
|
||||
);
|
||||
|
||||
|
|
|
@ -5,7 +5,10 @@ import { filter, first, takeUntil } from "rxjs/operators";
|
|||
|
||||
import { TwoFactorComponent as BaseTwoFactorComponent } from "@bitwarden/angular/auth/components/two-factor.component";
|
||||
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
|
||||
import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import {
|
||||
LoginStrategyServiceAbstraction,
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
|
@ -55,6 +58,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
|
|||
twoFactorService: TwoFactorService,
|
||||
appIdService: AppIdService,
|
||||
loginService: LoginService,
|
||||
userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
||||
configService: ConfigServiceAbstraction,
|
||||
ssoLoginService: SsoLoginServiceAbstraction,
|
||||
private dialogService: DialogService,
|
||||
|
@ -75,6 +79,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
|
|||
twoFactorService,
|
||||
appIdService,
|
||||
loginService,
|
||||
userDecryptionOptionsService,
|
||||
ssoLoginService,
|
||||
configService,
|
||||
);
|
||||
|
|
|
@ -5,6 +5,8 @@ import {
|
|||
PinCryptoService,
|
||||
LoginStrategyServiceAbstraction,
|
||||
LoginStrategyService,
|
||||
InternalUserDecryptionOptionsServiceAbstraction,
|
||||
UserDecryptionOptionsService,
|
||||
AuthRequestServiceAbstraction,
|
||||
AuthRequestService,
|
||||
} from "@bitwarden/auth/common";
|
||||
|
@ -242,6 +244,7 @@ export default class MainBackground {
|
|||
environmentService: BrowserEnvironmentService;
|
||||
cipherService: CipherServiceAbstraction;
|
||||
folderService: InternalFolderServiceAbstraction;
|
||||
userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction;
|
||||
collectionService: CollectionServiceAbstraction;
|
||||
vaultTimeoutService: VaultTimeoutService;
|
||||
vaultTimeoutSettingsService: VaultTimeoutSettingsServiceAbstraction;
|
||||
|
@ -539,6 +542,8 @@ export default class MainBackground {
|
|||
};
|
||||
})();
|
||||
|
||||
this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider);
|
||||
|
||||
this.devicesApiService = new DevicesApiServiceImplementation(this.apiService);
|
||||
this.deviceTrustCryptoService = new DeviceTrustCryptoService(
|
||||
this.keyGenerationService,
|
||||
|
@ -550,6 +555,7 @@ export default class MainBackground {
|
|||
this.devicesApiService,
|
||||
this.i18nService,
|
||||
this.platformUtilsService,
|
||||
this.userDecryptionOptionsService,
|
||||
);
|
||||
|
||||
this.devicesService = new DevicesServiceImplementation(this.devicesApiService);
|
||||
|
@ -590,6 +596,7 @@ export default class MainBackground {
|
|||
this.policyService,
|
||||
this.deviceTrustCryptoService,
|
||||
this.authRequestService,
|
||||
this.userDecryptionOptionsService,
|
||||
this.globalStateProvider,
|
||||
this.billingAccountProfileStateService,
|
||||
);
|
||||
|
@ -631,6 +638,7 @@ export default class MainBackground {
|
|||
this.folderApiService = new FolderApiService(this.folderService, this.apiService);
|
||||
|
||||
this.vaultTimeoutSettingsService = new VaultTimeoutSettingsService(
|
||||
this.userDecryptionOptionsService,
|
||||
this.cryptoService,
|
||||
this.tokenService,
|
||||
this.policyService,
|
||||
|
@ -650,6 +658,7 @@ export default class MainBackground {
|
|||
this.cryptoService,
|
||||
this.i18nService,
|
||||
this.userVerificationApiService,
|
||||
this.userDecryptionOptionsService,
|
||||
this.pinCryptoService,
|
||||
this.logService,
|
||||
this.vaultTimeoutSettingsService,
|
||||
|
@ -717,6 +726,7 @@ export default class MainBackground {
|
|||
this.folderApiService,
|
||||
this.organizationService,
|
||||
this.sendApiService,
|
||||
this.userDecryptionOptionsService,
|
||||
this.avatarService,
|
||||
logoutCallback,
|
||||
this.billingAccountProfileStateService,
|
||||
|
|
|
@ -9,6 +9,10 @@ import {
|
|||
tokenServiceFactory,
|
||||
TokenServiceInitOptions,
|
||||
} from "../../auth/background/service-factories/token-service.factory";
|
||||
import {
|
||||
userDecryptionOptionsServiceFactory,
|
||||
UserDecryptionOptionsServiceInitOptions,
|
||||
} from "../../auth/background/service-factories/user-decryption-options-service.factory";
|
||||
import {
|
||||
biometricStateServiceFactory,
|
||||
BiometricStateServiceInitOptions,
|
||||
|
@ -30,6 +34,7 @@ import {
|
|||
type VaultTimeoutSettingsServiceFactoryOptions = FactoryOptions;
|
||||
|
||||
export type VaultTimeoutSettingsServiceInitOptions = VaultTimeoutSettingsServiceFactoryOptions &
|
||||
UserDecryptionOptionsServiceInitOptions &
|
||||
CryptoServiceInitOptions &
|
||||
TokenServiceInitOptions &
|
||||
PolicyServiceInitOptions &
|
||||
|
@ -46,6 +51,7 @@ export function vaultTimeoutSettingsServiceFactory(
|
|||
opts,
|
||||
async () =>
|
||||
new VaultTimeoutSettingsService(
|
||||
await userDecryptionOptionsServiceFactory(cache, opts),
|
||||
await cryptoServiceFactory(cache, opts),
|
||||
await tokenServiceFactory(cache, opts),
|
||||
await policyServiceFactory(cache, opts),
|
||||
|
|
|
@ -5,11 +5,13 @@ import { program } from "commander";
|
|||
import * as jsdom from "jsdom";
|
||||
|
||||
import {
|
||||
InternalUserDecryptionOptionsServiceAbstraction,
|
||||
AuthRequestService,
|
||||
LoginStrategyService,
|
||||
LoginStrategyServiceAbstraction,
|
||||
PinCryptoService,
|
||||
PinCryptoServiceAbstraction,
|
||||
UserDecryptionOptionsService,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/common/abstractions/event/event-upload.service";
|
||||
|
@ -169,6 +171,7 @@ export class Main {
|
|||
eventUploadService: EventUploadServiceAbstraction;
|
||||
passwordGenerationService: PasswordGenerationServiceAbstraction;
|
||||
passwordStrengthService: PasswordStrengthServiceAbstraction;
|
||||
userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction;
|
||||
totpService: TotpService;
|
||||
containerService: ContainerService;
|
||||
auditService: AuditService;
|
||||
|
@ -436,6 +439,8 @@ export class Main {
|
|||
this.stateService,
|
||||
);
|
||||
|
||||
this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider);
|
||||
|
||||
this.devicesApiService = new DevicesApiServiceImplementation(this.apiService);
|
||||
this.deviceTrustCryptoService = new DeviceTrustCryptoService(
|
||||
this.keyGenerationService,
|
||||
|
@ -447,6 +452,7 @@ export class Main {
|
|||
this.devicesApiService,
|
||||
this.i18nService,
|
||||
this.platformUtilsService,
|
||||
this.userDecryptionOptionsService,
|
||||
);
|
||||
|
||||
this.authRequestService = new AuthRequestService(
|
||||
|
@ -478,6 +484,7 @@ export class Main {
|
|||
this.policyService,
|
||||
this.deviceTrustCryptoService,
|
||||
this.authRequestService,
|
||||
this.userDecryptionOptionsService,
|
||||
this.globalStateProvider,
|
||||
this.billingAccountProfileStateService,
|
||||
);
|
||||
|
@ -529,6 +536,7 @@ export class Main {
|
|||
this.biometricStateService = new DefaultBiometricStateService(this.stateProvider);
|
||||
|
||||
this.vaultTimeoutSettingsService = new VaultTimeoutSettingsService(
|
||||
this.userDecryptionOptionsService,
|
||||
this.cryptoService,
|
||||
this.tokenService,
|
||||
this.policyService,
|
||||
|
@ -548,6 +556,7 @@ export class Main {
|
|||
this.cryptoService,
|
||||
this.i18nService,
|
||||
this.userVerificationApiService,
|
||||
this.userDecryptionOptionsService,
|
||||
this.pinCryptoService,
|
||||
this.logService,
|
||||
this.vaultTimeoutSettingsService,
|
||||
|
@ -589,6 +598,7 @@ export class Main {
|
|||
this.folderApiService,
|
||||
this.organizationService,
|
||||
this.sendApiService,
|
||||
this.userDecryptionOptionsService,
|
||||
this.avatarService,
|
||||
async (expired: boolean) => await this.logout(),
|
||||
this.billingAccountProfileStateService,
|
||||
|
|
|
@ -2,6 +2,7 @@ import { Component, NgZone, OnDestroy } from "@angular/core";
|
|||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
|
||||
import { SetPasswordComponent as BaseSetPasswordComponent } from "@bitwarden/angular/auth/components/set-password.component";
|
||||
import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
|
||||
|
@ -44,6 +45,7 @@ export class SetPasswordComponent extends BaseSetPasswordComponent implements On
|
|||
stateService: StateService,
|
||||
organizationApiService: OrganizationApiServiceAbstraction,
|
||||
organizationUserService: OrganizationUserService,
|
||||
userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
|
||||
ssoLoginService: SsoLoginServiceAbstraction,
|
||||
dialogService: DialogService,
|
||||
) {
|
||||
|
@ -62,6 +64,7 @@ export class SetPasswordComponent extends BaseSetPasswordComponent implements On
|
|||
stateService,
|
||||
organizationApiService,
|
||||
organizationUserService,
|
||||
userDecryptionOptionsService,
|
||||
ssoLoginService,
|
||||
dialogService,
|
||||
);
|
||||
|
|
|
@ -2,7 +2,10 @@ import { Component } from "@angular/core";
|
|||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
|
||||
import { SsoComponent as BaseSsoComponent } from "@bitwarden/angular/auth/components/sso.component";
|
||||
import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import {
|
||||
LoginStrategyServiceAbstraction,
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
||||
|
@ -34,6 +37,7 @@ export class SsoComponent extends BaseSsoComponent {
|
|||
environmentService: EnvironmentService,
|
||||
passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||
logService: LogService,
|
||||
userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
||||
configService: ConfigServiceAbstraction,
|
||||
) {
|
||||
super(
|
||||
|
@ -49,6 +53,7 @@ export class SsoComponent extends BaseSsoComponent {
|
|||
environmentService,
|
||||
passwordGenerationService,
|
||||
logService,
|
||||
userDecryptionOptionsService,
|
||||
configService,
|
||||
);
|
||||
super.onSuccessfulLogin = async () => {
|
||||
|
|
|
@ -4,7 +4,10 @@ import { ActivatedRoute, Router } from "@angular/router";
|
|||
import { TwoFactorComponent as BaseTwoFactorComponent } from "@bitwarden/angular/auth/components/two-factor.component";
|
||||
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import {
|
||||
LoginStrategyServiceAbstraction,
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
|
@ -53,6 +56,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
|
|||
twoFactorService: TwoFactorService,
|
||||
appIdService: AppIdService,
|
||||
loginService: LoginService,
|
||||
userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
||||
ssoLoginService: SsoLoginServiceAbstraction,
|
||||
configService: ConfigServiceAbstraction,
|
||||
@Inject(WINDOW) protected win: Window,
|
||||
|
@ -71,6 +75,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
|
|||
twoFactorService,
|
||||
appIdService,
|
||||
loginService,
|
||||
userDecryptionOptionsService,
|
||||
ssoLoginService,
|
||||
configService,
|
||||
);
|
||||
|
|
|
@ -3,7 +3,10 @@ import { ActivatedRoute, Router } from "@angular/router";
|
|||
import { first } from "rxjs/operators";
|
||||
|
||||
import { SsoComponent as BaseSsoComponent } from "@bitwarden/angular/auth/components/sso.component";
|
||||
import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import {
|
||||
LoginStrategyServiceAbstraction,
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrgDomainApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization-domain/org-domain-api.service.abstraction";
|
||||
import { OrganizationDomainSsoDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-domain/responses/organization-domain-sso-details.response";
|
||||
|
@ -41,6 +44,7 @@ export class SsoComponent extends BaseSsoComponent {
|
|||
logService: LogService,
|
||||
private orgDomainApiService: OrgDomainApiServiceAbstraction,
|
||||
private validationService: ValidationService,
|
||||
userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
||||
configService: ConfigServiceAbstraction,
|
||||
) {
|
||||
super(
|
||||
|
@ -56,6 +60,7 @@ export class SsoComponent extends BaseSsoComponent {
|
|||
environmentService,
|
||||
passwordGenerationService,
|
||||
logService,
|
||||
userDecryptionOptionsService,
|
||||
configService,
|
||||
);
|
||||
this.redirectUri = window.location.origin + "/sso-connector.html";
|
||||
|
|
|
@ -4,7 +4,10 @@ import { ActivatedRoute, Router } from "@angular/router";
|
|||
import { TwoFactorComponent as BaseTwoFactorComponent } from "@bitwarden/angular/auth/components/two-factor.component";
|
||||
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import {
|
||||
LoginStrategyServiceAbstraction,
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
|
@ -44,6 +47,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnDest
|
|||
twoFactorService: TwoFactorService,
|
||||
appIdService: AppIdService,
|
||||
loginService: LoginService,
|
||||
userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
||||
ssoLoginService: SsoLoginServiceAbstraction,
|
||||
configService: ConfigServiceAbstraction,
|
||||
@Inject(WINDOW) protected win: Window,
|
||||
|
@ -62,6 +66,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnDest
|
|||
twoFactorService,
|
||||
appIdService,
|
||||
loginService,
|
||||
userDecryptionOptionsService,
|
||||
ssoLoginService,
|
||||
configService,
|
||||
);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Component, Inject, OnDestroy, OnInit } from "@angular/core";
|
||||
import { combineLatest, map, Observable, Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
|
||||
|
@ -12,7 +13,6 @@ import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
|||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
|
@ -44,8 +44,8 @@ export class OrganizationOptionsComponent implements OnInit, OnDestroy {
|
|||
private logService: LogService,
|
||||
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||
private organizationUserService: OrganizationUserService,
|
||||
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
||||
private dialogService: DialogService,
|
||||
private stateService: StateService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
|
@ -56,7 +56,7 @@ export class OrganizationOptionsComponent implements OnInit, OnDestroy {
|
|||
combineLatest([
|
||||
this.organization$,
|
||||
resetPasswordPolicies$,
|
||||
this.stateService.getAccountDecryptionOptions(),
|
||||
this.userDecryptionOptionsService.userDecryptionOptions$,
|
||||
])
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe(([organization, resetPasswordPolicies, decryptionOptions]) => {
|
||||
|
|
|
@ -14,6 +14,10 @@ import {
|
|||
throwError,
|
||||
} from "rxjs";
|
||||
|
||||
import {
|
||||
UserDecryptionOptions,
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
|
||||
|
@ -30,7 +34,6 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
|
|||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
import { AccountDecryptionOptions } from "@bitwarden/common/platform/models/domain/account";
|
||||
|
||||
enum State {
|
||||
NewUser,
|
||||
|
@ -88,6 +91,7 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy {
|
|||
protected validationService: ValidationService,
|
||||
protected deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
|
||||
protected platformUtilsService: PlatformUtilsService,
|
||||
protected userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
||||
protected passwordResetEnrollmentService: PasswordResetEnrollmentServiceAbstraction,
|
||||
protected ssoLoginService: SsoLoginServiceAbstraction,
|
||||
) {}
|
||||
|
@ -101,14 +105,15 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy {
|
|||
await this.setRememberDeviceDefaultValue();
|
||||
|
||||
try {
|
||||
const accountDecryptionOptions: AccountDecryptionOptions =
|
||||
await this.stateService.getAccountDecryptionOptions();
|
||||
const userDecryptionOptions = await firstValueFrom(
|
||||
this.userDecryptionOptionsService.userDecryptionOptions$,
|
||||
);
|
||||
|
||||
// see sso-login.strategy - to determine if a user is new or not it just checks if there is a key on the token response..
|
||||
// can we check if they have a user key or master key in crypto service? Would that be sufficient?
|
||||
if (
|
||||
!accountDecryptionOptions?.trustedDeviceOption?.hasAdminApproval &&
|
||||
!accountDecryptionOptions?.hasMasterPassword
|
||||
!userDecryptionOptions?.trustedDeviceOption?.hasAdminApproval &&
|
||||
!userDecryptionOptions?.hasMasterPassword
|
||||
) {
|
||||
// We are dealing with a new account if:
|
||||
// - User does not have admin approval (i.e. has not enrolled into admin reset)
|
||||
|
@ -118,7 +123,7 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy {
|
|||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.loadNewUserData();
|
||||
} else {
|
||||
this.loadUntrustedDeviceData(accountDecryptionOptions);
|
||||
this.loadUntrustedDeviceData(userDecryptionOptions);
|
||||
}
|
||||
|
||||
// Note: this is probably not a comprehensive write up of all scenarios:
|
||||
|
@ -195,7 +200,7 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy {
|
|||
this.loading = false;
|
||||
}
|
||||
|
||||
loadUntrustedDeviceData(accountDecryptionOptions: AccountDecryptionOptions) {
|
||||
loadUntrustedDeviceData(userDecryptionOptions: UserDecryptionOptions) {
|
||||
this.loading = true;
|
||||
|
||||
const email$ = from(this.stateService.getEmail()).pipe(
|
||||
|
@ -215,13 +220,12 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy {
|
|||
)
|
||||
.subscribe((email) => {
|
||||
const showApproveFromOtherDeviceBtn =
|
||||
accountDecryptionOptions?.trustedDeviceOption?.hasLoginApprovingDevice || false;
|
||||
userDecryptionOptions?.trustedDeviceOption?.hasLoginApprovingDevice || false;
|
||||
|
||||
const showReqAdminApprovalBtn =
|
||||
!!accountDecryptionOptions?.trustedDeviceOption?.hasAdminApproval || false;
|
||||
!!userDecryptionOptions?.trustedDeviceOption?.hasAdminApproval || false;
|
||||
|
||||
const showApproveWithMasterPasswordBtn =
|
||||
accountDecryptionOptions?.hasMasterPassword || false;
|
||||
const showApproveWithMasterPasswordBtn = userDecryptionOptions?.hasMasterPassword || false;
|
||||
|
||||
const userEmail = email;
|
||||
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { Directive } from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { of } from "rxjs";
|
||||
import { firstValueFrom, of } from "rxjs";
|
||||
import { filter, first, switchMap, tap } from "rxjs/operators";
|
||||
|
||||
import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
|
||||
|
@ -26,7 +27,6 @@ import {
|
|||
DEFAULT_KDF_CONFIG,
|
||||
} from "@bitwarden/common/platform/enums";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { AccountDecryptionOptions } from "@bitwarden/common/platform/models/domain/account";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
|
||||
|
@ -64,6 +64,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent {
|
|||
stateService: StateService,
|
||||
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||
private organizationUserService: OrganizationUserService,
|
||||
private userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
|
||||
private ssoLoginService: SsoLoginServiceAbstraction,
|
||||
dialogService: DialogService,
|
||||
) {
|
||||
|
@ -228,11 +229,11 @@ export class SetPasswordComponent extends BaseChangePasswordComponent {
|
|||
await this.stateService.setForceSetPasswordReason(ForceSetPasswordReason.None);
|
||||
|
||||
// User now has a password so update account decryption options in state
|
||||
const acctDecryptionOpts: AccountDecryptionOptions =
|
||||
await this.stateService.getAccountDecryptionOptions();
|
||||
|
||||
acctDecryptionOpts.hasMasterPassword = true;
|
||||
await this.stateService.setAccountDecryptionOptions(acctDecryptionOpts);
|
||||
const userDecryptionOpts = await firstValueFrom(
|
||||
this.userDecryptionOptionsService.userDecryptionOptions$,
|
||||
);
|
||||
userDecryptionOpts.hasMasterPassword = true;
|
||||
await this.userDecryptionOptionsService.setUserDecryptionOptions(userDecryptionOpts);
|
||||
|
||||
await this.stateService.setKdfType(this.kdf);
|
||||
await this.stateService.setKdfConfig(this.kdfConfig);
|
||||
|
|
|
@ -2,16 +2,20 @@ import { Component } from "@angular/core";
|
|||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { MockProxy, mock } from "jest-mock-extended";
|
||||
import { Observable, of } from "rxjs";
|
||||
import { BehaviorSubject, Observable, of } from "rxjs";
|
||||
|
||||
import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import {
|
||||
FakeKeyConnectorUserDecryptionOption as KeyConnectorUserDecryptionOption,
|
||||
LoginStrategyServiceAbstraction,
|
||||
FakeTrustedDeviceUserDecryptionOption as TrustedDeviceUserDecryptionOption,
|
||||
FakeUserDecryptionOptions as UserDecryptionOptions,
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { KeyConnectorUserDecryptionOption } from "@bitwarden/common/auth/models/domain/user-decryption-options/key-connector-user-decryption-option";
|
||||
import { TrustedDeviceUserDecryptionOption } from "@bitwarden/common/auth/models/domain/user-decryption-options/trusted-device-user-decryption-option";
|
||||
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
|
@ -19,7 +23,6 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
|||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { AccountDecryptionOptions } from "@bitwarden/common/platform/models/domain/account";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||
|
||||
import { SsoComponent } from "./sso.component";
|
||||
|
@ -62,6 +65,7 @@ describe("SsoComponent", () => {
|
|||
let mockEnvironmentService: MockProxy<EnvironmentService>;
|
||||
let mockPasswordGenerationService: MockProxy<PasswordGenerationServiceAbstraction>;
|
||||
let mockLogService: MockProxy<LogService>;
|
||||
let mockUserDecryptionOptionsService: MockProxy<UserDecryptionOptionsServiceAbstraction>;
|
||||
let mockConfigService: MockProxy<ConfigServiceAbstraction>;
|
||||
|
||||
// Mock authService.logIn params
|
||||
|
@ -77,17 +81,19 @@ describe("SsoComponent", () => {
|
|||
let mockOnSuccessfulLoginForceResetNavigate: jest.Mock;
|
||||
let mockOnSuccessfulLoginTdeNavigate: jest.Mock;
|
||||
|
||||
let mockAcctDecryptionOpts: {
|
||||
noMasterPassword: AccountDecryptionOptions;
|
||||
withMasterPassword: AccountDecryptionOptions;
|
||||
withMasterPasswordAndTrustedDevice: AccountDecryptionOptions;
|
||||
withMasterPasswordAndTrustedDeviceWithManageResetPassword: AccountDecryptionOptions;
|
||||
withMasterPasswordAndKeyConnector: AccountDecryptionOptions;
|
||||
noMasterPasswordWithTrustedDevice: AccountDecryptionOptions;
|
||||
noMasterPasswordWithTrustedDeviceWithManageResetPassword: AccountDecryptionOptions;
|
||||
noMasterPasswordWithKeyConnector: AccountDecryptionOptions;
|
||||
let mockUserDecryptionOpts: {
|
||||
noMasterPassword: UserDecryptionOptions;
|
||||
withMasterPassword: UserDecryptionOptions;
|
||||
withMasterPasswordAndTrustedDevice: UserDecryptionOptions;
|
||||
withMasterPasswordAndTrustedDeviceWithManageResetPassword: UserDecryptionOptions;
|
||||
withMasterPasswordAndKeyConnector: UserDecryptionOptions;
|
||||
noMasterPasswordWithTrustedDevice: UserDecryptionOptions;
|
||||
noMasterPasswordWithTrustedDeviceWithManageResetPassword: UserDecryptionOptions;
|
||||
noMasterPasswordWithKeyConnector: UserDecryptionOptions;
|
||||
};
|
||||
|
||||
let selectedUserDecryptionOptions: BehaviorSubject<UserDecryptionOptions>;
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock Services
|
||||
mockLoginStrategyService = mock<LoginStrategyServiceAbstraction>();
|
||||
|
@ -109,6 +115,7 @@ describe("SsoComponent", () => {
|
|||
mockEnvironmentService = mock<EnvironmentService>();
|
||||
mockPasswordGenerationService = mock<PasswordGenerationServiceAbstraction>();
|
||||
mockLogService = mock<LogService>();
|
||||
mockUserDecryptionOptionsService = mock<UserDecryptionOptionsServiceAbstraction>();
|
||||
mockConfigService = mock<ConfigServiceAbstraction>();
|
||||
|
||||
// Mock loginStrategyService.logIn params
|
||||
|
@ -124,49 +131,52 @@ describe("SsoComponent", () => {
|
|||
mockOnSuccessfulLoginForceResetNavigate = jest.fn();
|
||||
mockOnSuccessfulLoginTdeNavigate = jest.fn();
|
||||
|
||||
mockAcctDecryptionOpts = {
|
||||
noMasterPassword: new AccountDecryptionOptions({
|
||||
mockUserDecryptionOpts = {
|
||||
noMasterPassword: new UserDecryptionOptions({
|
||||
hasMasterPassword: false,
|
||||
trustedDeviceOption: undefined,
|
||||
keyConnectorOption: undefined,
|
||||
}),
|
||||
withMasterPassword: new AccountDecryptionOptions({
|
||||
withMasterPassword: new UserDecryptionOptions({
|
||||
hasMasterPassword: true,
|
||||
trustedDeviceOption: undefined,
|
||||
keyConnectorOption: undefined,
|
||||
}),
|
||||
withMasterPasswordAndTrustedDevice: new AccountDecryptionOptions({
|
||||
withMasterPasswordAndTrustedDevice: new UserDecryptionOptions({
|
||||
hasMasterPassword: true,
|
||||
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, false),
|
||||
keyConnectorOption: undefined,
|
||||
}),
|
||||
withMasterPasswordAndTrustedDeviceWithManageResetPassword: new AccountDecryptionOptions({
|
||||
withMasterPasswordAndTrustedDeviceWithManageResetPassword: new UserDecryptionOptions({
|
||||
hasMasterPassword: true,
|
||||
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, true),
|
||||
keyConnectorOption: undefined,
|
||||
}),
|
||||
withMasterPasswordAndKeyConnector: new AccountDecryptionOptions({
|
||||
withMasterPasswordAndKeyConnector: new UserDecryptionOptions({
|
||||
hasMasterPassword: true,
|
||||
trustedDeviceOption: undefined,
|
||||
keyConnectorOption: new KeyConnectorUserDecryptionOption("http://example.com"),
|
||||
}),
|
||||
noMasterPasswordWithTrustedDevice: new AccountDecryptionOptions({
|
||||
noMasterPasswordWithTrustedDevice: new UserDecryptionOptions({
|
||||
hasMasterPassword: false,
|
||||
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, false),
|
||||
keyConnectorOption: undefined,
|
||||
}),
|
||||
noMasterPasswordWithTrustedDeviceWithManageResetPassword: new AccountDecryptionOptions({
|
||||
noMasterPasswordWithTrustedDeviceWithManageResetPassword: new UserDecryptionOptions({
|
||||
hasMasterPassword: false,
|
||||
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, true),
|
||||
keyConnectorOption: undefined,
|
||||
}),
|
||||
noMasterPasswordWithKeyConnector: new AccountDecryptionOptions({
|
||||
noMasterPasswordWithKeyConnector: new UserDecryptionOptions({
|
||||
hasMasterPassword: false,
|
||||
trustedDeviceOption: undefined,
|
||||
keyConnectorOption: new KeyConnectorUserDecryptionOption("http://example.com"),
|
||||
}),
|
||||
};
|
||||
|
||||
selectedUserDecryptionOptions = new BehaviorSubject<UserDecryptionOptions>(null);
|
||||
mockUserDecryptionOptionsService.userDecryptionOptions$ = selectedUserDecryptionOptions;
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [TestSsoComponent],
|
||||
providers: [
|
||||
|
@ -183,6 +193,10 @@ describe("SsoComponent", () => {
|
|||
{ provide: EnvironmentService, useValue: mockEnvironmentService },
|
||||
{ provide: PasswordGenerationServiceAbstraction, useValue: mockPasswordGenerationService },
|
||||
|
||||
{
|
||||
provide: UserDecryptionOptionsServiceAbstraction,
|
||||
useValue: mockUserDecryptionOptionsService,
|
||||
},
|
||||
{ provide: LogService, useValue: mockLogService },
|
||||
{ provide: ConfigServiceAbstraction, useValue: mockConfigService },
|
||||
],
|
||||
|
@ -230,9 +244,7 @@ describe("SsoComponent", () => {
|
|||
authResult.twoFactorProviders = new Map([[TwoFactorProviderType.Authenticator, {}]]);
|
||||
|
||||
// use standard user with MP because this test is not concerned with password reset.
|
||||
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
|
||||
mockAcctDecryptionOpts.withMasterPassword,
|
||||
);
|
||||
selectedUserDecryptionOptions.next(mockUserDecryptionOpts.withMasterPassword);
|
||||
|
||||
mockLoginStrategyService.logIn.mockResolvedValue(authResult);
|
||||
});
|
||||
|
@ -341,8 +353,8 @@ describe("SsoComponent", () => {
|
|||
describe("Given Trusted Device Encryption is enabled and user needs to set a master password", () => {
|
||||
let authResult;
|
||||
beforeEach(() => {
|
||||
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
|
||||
mockAcctDecryptionOpts.noMasterPasswordWithTrustedDeviceWithManageResetPassword,
|
||||
selectedUserDecryptionOptions.next(
|
||||
mockUserDecryptionOpts.noMasterPasswordWithTrustedDeviceWithManageResetPassword,
|
||||
);
|
||||
|
||||
authResult = new AuthResult();
|
||||
|
@ -377,8 +389,8 @@ describe("SsoComponent", () => {
|
|||
const reasonString = ForceSetPasswordReason[forceResetPasswordReason];
|
||||
let authResult;
|
||||
beforeEach(() => {
|
||||
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
|
||||
mockAcctDecryptionOpts.withMasterPasswordAndTrustedDevice,
|
||||
selectedUserDecryptionOptions.next(
|
||||
mockUserDecryptionOpts.withMasterPasswordAndTrustedDevice,
|
||||
);
|
||||
|
||||
authResult = new AuthResult();
|
||||
|
@ -394,8 +406,8 @@ describe("SsoComponent", () => {
|
|||
describe("Given Trusted Device Encryption is enabled, user doesn't need to set a MP, and forcePasswordReset is not required", () => {
|
||||
let authResult;
|
||||
beforeEach(() => {
|
||||
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
|
||||
mockAcctDecryptionOpts.withMasterPasswordAndTrustedDevice,
|
||||
selectedUserDecryptionOptions.next(
|
||||
mockUserDecryptionOpts.withMasterPasswordAndTrustedDevice,
|
||||
);
|
||||
|
||||
authResult = new AuthResult();
|
||||
|
@ -440,9 +452,7 @@ describe("SsoComponent", () => {
|
|||
describe("Given user needs to set a master password", () => {
|
||||
beforeEach(() => {
|
||||
// Only need to test the case where the user has no master password to test the primary change mp flow here
|
||||
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
|
||||
mockAcctDecryptionOpts.noMasterPassword,
|
||||
);
|
||||
selectedUserDecryptionOptions.next(mockUserDecryptionOpts.noMasterPassword);
|
||||
});
|
||||
|
||||
testChangePasswordOnSuccessfulLogin();
|
||||
|
@ -450,9 +460,7 @@ describe("SsoComponent", () => {
|
|||
});
|
||||
|
||||
it("does not navigate to the change password route when the user has key connector even if user has no master password", async () => {
|
||||
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
|
||||
mockAcctDecryptionOpts.noMasterPasswordWithKeyConnector,
|
||||
);
|
||||
selectedUserDecryptionOptions.next(mockUserDecryptionOpts.noMasterPasswordWithKeyConnector);
|
||||
|
||||
await _component.logIn(code, codeVerifier, orgIdFromState);
|
||||
expect(mockLoginStrategyService.logIn).toHaveBeenCalledTimes(1);
|
||||
|
@ -475,9 +483,7 @@ describe("SsoComponent", () => {
|
|||
|
||||
beforeEach(() => {
|
||||
// use standard user with MP because this test is not concerned with password reset.
|
||||
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
|
||||
mockAcctDecryptionOpts.withMasterPassword,
|
||||
);
|
||||
selectedUserDecryptionOptions.next(mockUserDecryptionOpts.withMasterPassword);
|
||||
|
||||
const authResult = new AuthResult();
|
||||
authResult.forcePasswordReset = forceResetPasswordReason;
|
||||
|
@ -494,9 +500,7 @@ describe("SsoComponent", () => {
|
|||
const authResult = new AuthResult();
|
||||
authResult.twoFactorProviders = null;
|
||||
// use standard user with MP because this test is not concerned with password reset.
|
||||
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
|
||||
mockAcctDecryptionOpts.withMasterPassword,
|
||||
);
|
||||
selectedUserDecryptionOptions.next(mockUserDecryptionOpts.withMasterPassword);
|
||||
authResult.forcePasswordReset = ForceSetPasswordReason.None;
|
||||
mockLoginStrategyService.logIn.mockResolvedValue(authResult);
|
||||
});
|
||||
|
|
|
@ -1,13 +1,19 @@
|
|||
import { Directive } from "@angular/core";
|
||||
import { ActivatedRoute, NavigationExtras, Router } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import { first } from "rxjs/operators";
|
||||
|
||||
import { LoginStrategyServiceAbstraction, SsoLoginCredentials } from "@bitwarden/auth/common";
|
||||
import {
|
||||
LoginStrategyServiceAbstraction,
|
||||
SsoLoginCredentials,
|
||||
TrustedDeviceUserDecryptionOption,
|
||||
UserDecryptionOptions,
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { TrustedDeviceUserDecryptionOption } from "@bitwarden/common/auth/models/domain/user-decryption-options/trusted-device-user-decryption-option";
|
||||
import { SsoPreValidateResponse } from "@bitwarden/common/auth/models/response/sso-pre-validate.response";
|
||||
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
|
@ -17,7 +23,6 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
|
|||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { AccountDecryptionOptions } from "@bitwarden/common/platform/models/domain/account";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||
|
||||
@Directive()
|
||||
|
@ -59,6 +64,7 @@ export class SsoComponent {
|
|||
protected environmentService: EnvironmentService,
|
||||
protected passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||
protected logService: LogService,
|
||||
protected userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
||||
protected configService: ConfigServiceAbstraction,
|
||||
) {}
|
||||
|
||||
|
@ -194,9 +200,6 @@ export class SsoComponent {
|
|||
this.formPromise = this.loginStrategyService.logIn(credentials);
|
||||
const authResult = await this.formPromise;
|
||||
|
||||
const acctDecryptionOpts: AccountDecryptionOptions =
|
||||
await this.stateService.getAccountDecryptionOptions();
|
||||
|
||||
if (authResult.requiresTwoFactor) {
|
||||
return await this.handleTwoFactorRequired(orgSsoIdentifier);
|
||||
}
|
||||
|
@ -217,15 +220,20 @@ export class SsoComponent {
|
|||
return await this.handleForcePasswordReset(orgSsoIdentifier);
|
||||
}
|
||||
|
||||
// must come after 2fa check since user decryption options aren't available if 2fa is required
|
||||
const userDecryptionOpts = await firstValueFrom(
|
||||
this.userDecryptionOptionsService.userDecryptionOptions$,
|
||||
);
|
||||
|
||||
const tdeEnabled = await this.isTrustedDeviceEncEnabled(
|
||||
acctDecryptionOpts.trustedDeviceOption,
|
||||
userDecryptionOpts.trustedDeviceOption,
|
||||
);
|
||||
|
||||
if (tdeEnabled) {
|
||||
return await this.handleTrustedDeviceEncryptionEnabled(
|
||||
authResult,
|
||||
orgSsoIdentifier,
|
||||
acctDecryptionOpts,
|
||||
userDecryptionOpts,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -233,8 +241,8 @@ export class SsoComponent {
|
|||
// have one and they aren't using key connector.
|
||||
// Note: TDE & Key connector are mutually exclusive org config options.
|
||||
const requireSetPassword =
|
||||
!acctDecryptionOpts.hasMasterPassword &&
|
||||
acctDecryptionOpts.keyConnectorOption === undefined;
|
||||
!userDecryptionOpts.hasMasterPassword &&
|
||||
userDecryptionOpts.keyConnectorOption === undefined;
|
||||
|
||||
if (requireSetPassword || authResult.resetMasterPassword) {
|
||||
// Change implies going no password -> password in this case
|
||||
|
@ -270,12 +278,12 @@ export class SsoComponent {
|
|||
private async handleTrustedDeviceEncryptionEnabled(
|
||||
authResult: AuthResult,
|
||||
orgIdentifier: string,
|
||||
acctDecryptionOpts: AccountDecryptionOptions,
|
||||
userDecryptionOpts: UserDecryptionOptions,
|
||||
): Promise<void> {
|
||||
// If user doesn't have a MP, but has reset password permission, they must set a MP
|
||||
if (
|
||||
!acctDecryptionOpts.hasMasterPassword &&
|
||||
acctDecryptionOpts.trustedDeviceOption.hasManageResetPasswordPermission
|
||||
!userDecryptionOpts.hasMasterPassword &&
|
||||
userDecryptionOpts.trustedDeviceOption.hasManageResetPasswordPermission
|
||||
) {
|
||||
// Set flag so that auth guard can redirect to set password screen after decryption (trusted or untrusted device)
|
||||
// Note: we cannot directly navigate in this scenario as we are in a pre-decryption state, and
|
||||
|
|
|
@ -1,19 +1,24 @@
|
|||
import { Component } from "@angular/core";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { ActivatedRoute, Router, convertToParamMap } from "@angular/router";
|
||||
import { MockProxy, mock } from "jest-mock-extended";
|
||||
import { ActivatedRoute, convertToParamMap, Router } from "@angular/router";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
|
||||
import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import {
|
||||
FakeKeyConnectorUserDecryptionOption as KeyConnectorUserDecryptionOption,
|
||||
LoginStrategyServiceAbstraction,
|
||||
FakeTrustedDeviceUserDecryptionOption as TrustedDeviceUserDecryptionOption,
|
||||
FakeUserDecryptionOptions as UserDecryptionOptions,
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { KeyConnectorUserDecryptionOption } from "@bitwarden/common/auth/models/domain/user-decryption-options/key-connector-user-decryption-option";
|
||||
import { TrustedDeviceUserDecryptionOption } from "@bitwarden/common/auth/models/domain/user-decryption-options/trusted-device-user-decryption-option";
|
||||
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
||||
|
@ -22,7 +27,6 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
|||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { AccountDecryptionOptions } from "@bitwarden/common/platform/models/domain/account";
|
||||
|
||||
import { TwoFactorComponent } from "./two-factor.component";
|
||||
|
||||
|
@ -56,20 +60,23 @@ describe("TwoFactorComponent", () => {
|
|||
let mockTwoFactorService: MockProxy<TwoFactorService>;
|
||||
let mockAppIdService: MockProxy<AppIdService>;
|
||||
let mockLoginService: MockProxy<LoginService>;
|
||||
let mockUserDecryptionOptionsService: MockProxy<UserDecryptionOptionsServiceAbstraction>;
|
||||
let mockSsoLoginService: MockProxy<SsoLoginServiceAbstraction>;
|
||||
let mockConfigService: MockProxy<ConfigServiceAbstraction>;
|
||||
|
||||
let mockAcctDecryptionOpts: {
|
||||
noMasterPassword: AccountDecryptionOptions;
|
||||
withMasterPassword: AccountDecryptionOptions;
|
||||
withMasterPasswordAndTrustedDevice: AccountDecryptionOptions;
|
||||
withMasterPasswordAndTrustedDeviceWithManageResetPassword: AccountDecryptionOptions;
|
||||
withMasterPasswordAndKeyConnector: AccountDecryptionOptions;
|
||||
noMasterPasswordWithTrustedDevice: AccountDecryptionOptions;
|
||||
noMasterPasswordWithTrustedDeviceWithManageResetPassword: AccountDecryptionOptions;
|
||||
noMasterPasswordWithKeyConnector: AccountDecryptionOptions;
|
||||
let mockUserDecryptionOpts: {
|
||||
noMasterPassword: UserDecryptionOptions;
|
||||
withMasterPassword: UserDecryptionOptions;
|
||||
withMasterPasswordAndTrustedDevice: UserDecryptionOptions;
|
||||
withMasterPasswordAndTrustedDeviceWithManageResetPassword: UserDecryptionOptions;
|
||||
withMasterPasswordAndKeyConnector: UserDecryptionOptions;
|
||||
noMasterPasswordWithTrustedDevice: UserDecryptionOptions;
|
||||
noMasterPasswordWithTrustedDeviceWithManageResetPassword: UserDecryptionOptions;
|
||||
noMasterPasswordWithKeyConnector: UserDecryptionOptions;
|
||||
};
|
||||
|
||||
let selectedUserDecryptionOptions: BehaviorSubject<UserDecryptionOptions>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockLoginStrategyService = mock<LoginStrategyServiceAbstraction>();
|
||||
mockRouter = mock<Router>();
|
||||
|
@ -83,52 +90,56 @@ describe("TwoFactorComponent", () => {
|
|||
mockTwoFactorService = mock<TwoFactorService>();
|
||||
mockAppIdService = mock<AppIdService>();
|
||||
mockLoginService = mock<LoginService>();
|
||||
mockUserDecryptionOptionsService = mock<UserDecryptionOptionsServiceAbstraction>();
|
||||
mockSsoLoginService = mock<SsoLoginServiceAbstraction>();
|
||||
mockConfigService = mock<ConfigServiceAbstraction>();
|
||||
|
||||
mockAcctDecryptionOpts = {
|
||||
noMasterPassword: new AccountDecryptionOptions({
|
||||
mockUserDecryptionOpts = {
|
||||
noMasterPassword: new UserDecryptionOptions({
|
||||
hasMasterPassword: false,
|
||||
trustedDeviceOption: undefined,
|
||||
keyConnectorOption: undefined,
|
||||
}),
|
||||
withMasterPassword: new AccountDecryptionOptions({
|
||||
withMasterPassword: new UserDecryptionOptions({
|
||||
hasMasterPassword: true,
|
||||
trustedDeviceOption: undefined,
|
||||
keyConnectorOption: undefined,
|
||||
}),
|
||||
withMasterPasswordAndTrustedDevice: new AccountDecryptionOptions({
|
||||
withMasterPasswordAndTrustedDevice: new UserDecryptionOptions({
|
||||
hasMasterPassword: true,
|
||||
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, false),
|
||||
keyConnectorOption: undefined,
|
||||
}),
|
||||
withMasterPasswordAndTrustedDeviceWithManageResetPassword: new AccountDecryptionOptions({
|
||||
withMasterPasswordAndTrustedDeviceWithManageResetPassword: new UserDecryptionOptions({
|
||||
hasMasterPassword: true,
|
||||
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, true),
|
||||
keyConnectorOption: undefined,
|
||||
}),
|
||||
withMasterPasswordAndKeyConnector: new AccountDecryptionOptions({
|
||||
withMasterPasswordAndKeyConnector: new UserDecryptionOptions({
|
||||
hasMasterPassword: true,
|
||||
trustedDeviceOption: undefined,
|
||||
keyConnectorOption: new KeyConnectorUserDecryptionOption("http://example.com"),
|
||||
}),
|
||||
noMasterPasswordWithTrustedDevice: new AccountDecryptionOptions({
|
||||
noMasterPasswordWithTrustedDevice: new UserDecryptionOptions({
|
||||
hasMasterPassword: false,
|
||||
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, false),
|
||||
keyConnectorOption: undefined,
|
||||
}),
|
||||
noMasterPasswordWithTrustedDeviceWithManageResetPassword: new AccountDecryptionOptions({
|
||||
noMasterPasswordWithTrustedDeviceWithManageResetPassword: new UserDecryptionOptions({
|
||||
hasMasterPassword: false,
|
||||
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, true),
|
||||
keyConnectorOption: undefined,
|
||||
}),
|
||||
noMasterPasswordWithKeyConnector: new AccountDecryptionOptions({
|
||||
noMasterPasswordWithKeyConnector: new UserDecryptionOptions({
|
||||
hasMasterPassword: false,
|
||||
trustedDeviceOption: undefined,
|
||||
keyConnectorOption: new KeyConnectorUserDecryptionOption("http://example.com"),
|
||||
}),
|
||||
};
|
||||
|
||||
selectedUserDecryptionOptions = new BehaviorSubject<UserDecryptionOptions>(null);
|
||||
mockUserDecryptionOptionsService.userDecryptionOptions$ = selectedUserDecryptionOptions;
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [TestTwoFactorComponent],
|
||||
providers: [
|
||||
|
@ -153,6 +164,10 @@ describe("TwoFactorComponent", () => {
|
|||
{ provide: TwoFactorService, useValue: mockTwoFactorService },
|
||||
{ provide: AppIdService, useValue: mockAppIdService },
|
||||
{ provide: LoginService, useValue: mockLoginService },
|
||||
{
|
||||
provide: UserDecryptionOptionsServiceAbstraction,
|
||||
useValue: mockUserDecryptionOptionsService,
|
||||
},
|
||||
{ provide: SsoLoginServiceAbstraction, useValue: mockSsoLoginService },
|
||||
{ provide: ConfigServiceAbstraction, useValue: mockConfigService },
|
||||
],
|
||||
|
@ -213,9 +228,7 @@ describe("TwoFactorComponent", () => {
|
|||
component.remember = remember;
|
||||
component.captchaToken = captchaToken;
|
||||
|
||||
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
|
||||
mockAcctDecryptionOpts.withMasterPassword,
|
||||
);
|
||||
selectedUserDecryptionOptions.next(mockUserDecryptionOpts.withMasterPassword);
|
||||
});
|
||||
|
||||
it("calls authService.logInTwoFactor with correct parameters when form is submitted", async () => {
|
||||
|
@ -289,17 +302,15 @@ describe("TwoFactorComponent", () => {
|
|||
describe("Given user needs to set a master password", () => {
|
||||
beforeEach(() => {
|
||||
// Only need to test the case where the user has no master password to test the primary change mp flow here
|
||||
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
|
||||
mockAcctDecryptionOpts.noMasterPassword,
|
||||
);
|
||||
selectedUserDecryptionOptions.next(mockUserDecryptionOpts.noMasterPassword);
|
||||
});
|
||||
|
||||
testChangePasswordOnSuccessfulLogin();
|
||||
});
|
||||
|
||||
it("does not navigate to the change password route when the user has key connector even if user has no master password", async () => {
|
||||
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
|
||||
mockAcctDecryptionOpts.noMasterPasswordWithKeyConnector,
|
||||
selectedUserDecryptionOptions.next(
|
||||
mockUserDecryptionOpts.noMasterPasswordWithKeyConnector,
|
||||
);
|
||||
|
||||
await component.doSubmit();
|
||||
|
@ -321,9 +332,7 @@ describe("TwoFactorComponent", () => {
|
|||
|
||||
beforeEach(() => {
|
||||
// use standard user with MP because this test is not concerned with password reset.
|
||||
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
|
||||
mockAcctDecryptionOpts.withMasterPassword,
|
||||
);
|
||||
selectedUserDecryptionOptions.next(mockUserDecryptionOpts.withMasterPassword);
|
||||
|
||||
const authResult = new AuthResult();
|
||||
authResult.forcePasswordReset = forceResetPasswordReason;
|
||||
|
@ -385,8 +394,8 @@ describe("TwoFactorComponent", () => {
|
|||
|
||||
describe("Given Trusted Device Encryption is enabled and user needs to set a master password", () => {
|
||||
beforeEach(() => {
|
||||
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
|
||||
mockAcctDecryptionOpts.noMasterPasswordWithTrustedDeviceWithManageResetPassword,
|
||||
selectedUserDecryptionOptions.next(
|
||||
mockUserDecryptionOpts.noMasterPasswordWithTrustedDeviceWithManageResetPassword,
|
||||
);
|
||||
|
||||
const authResult = new AuthResult();
|
||||
|
@ -420,8 +429,8 @@ describe("TwoFactorComponent", () => {
|
|||
|
||||
beforeEach(() => {
|
||||
// use standard user with MP because this test is not concerned with password reset.
|
||||
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
|
||||
mockAcctDecryptionOpts.withMasterPasswordAndTrustedDevice,
|
||||
selectedUserDecryptionOptions.next(
|
||||
mockUserDecryptionOpts.withMasterPasswordAndTrustedDevice,
|
||||
);
|
||||
|
||||
const authResult = new AuthResult();
|
||||
|
@ -436,8 +445,8 @@ describe("TwoFactorComponent", () => {
|
|||
describe("Given Trusted Device Encryption is enabled, user doesn't need to set a MP, and forcePasswordReset is not required", () => {
|
||||
let authResult;
|
||||
beforeEach(() => {
|
||||
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
|
||||
mockAcctDecryptionOpts.withMasterPasswordAndTrustedDevice,
|
||||
selectedUserDecryptionOptions.next(
|
||||
mockUserDecryptionOpts.withMasterPasswordAndTrustedDevice,
|
||||
);
|
||||
|
||||
authResult = new AuthResult();
|
||||
|
|
|
@ -6,7 +6,12 @@ import { first } from "rxjs/operators";
|
|||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
|
||||
import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import {
|
||||
LoginStrategyServiceAbstraction,
|
||||
TrustedDeviceUserDecryptionOption,
|
||||
UserDecryptionOptions,
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
|
@ -15,7 +20,6 @@ import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-
|
|||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { TrustedDeviceUserDecryptionOption } from "@bitwarden/common/auth/models/domain/user-decryption-options/trusted-device-user-decryption-option";
|
||||
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
|
||||
import { TwoFactorEmailRequest } from "@bitwarden/common/auth/models/request/two-factor-email.request";
|
||||
import { TwoFactorProviders } from "@bitwarden/common/auth/services/two-factor.service";
|
||||
|
@ -27,7 +31,6 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
|||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { AccountDecryptionOptions } from "@bitwarden/common/platform/models/domain/account";
|
||||
|
||||
import { CaptchaProtectedComponent } from "./captcha-protected.component";
|
||||
|
||||
|
@ -86,6 +89,7 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
|
|||
protected twoFactorService: TwoFactorService,
|
||||
protected appIdService: AppIdService,
|
||||
protected loginService: LoginService,
|
||||
protected userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
||||
protected ssoLoginService: SsoLoginServiceAbstraction,
|
||||
protected configService: ConfigServiceAbstraction,
|
||||
) {
|
||||
|
@ -290,22 +294,23 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
|
|||
return await this.handleForcePasswordReset(this.orgIdentifier);
|
||||
}
|
||||
|
||||
const acctDecryptionOpts: AccountDecryptionOptions =
|
||||
await this.stateService.getAccountDecryptionOptions();
|
||||
const userDecryptionOpts = await firstValueFrom(
|
||||
this.userDecryptionOptionsService.userDecryptionOptions$,
|
||||
);
|
||||
|
||||
const tdeEnabled = await this.isTrustedDeviceEncEnabled(acctDecryptionOpts.trustedDeviceOption);
|
||||
const tdeEnabled = await this.isTrustedDeviceEncEnabled(userDecryptionOpts.trustedDeviceOption);
|
||||
|
||||
if (tdeEnabled) {
|
||||
return await this.handleTrustedDeviceEncryptionEnabled(
|
||||
authResult,
|
||||
this.orgIdentifier,
|
||||
acctDecryptionOpts,
|
||||
userDecryptionOpts,
|
||||
);
|
||||
}
|
||||
|
||||
// User must set password if they don't have one and they aren't using either TDE or key connector.
|
||||
const requireSetPassword =
|
||||
!acctDecryptionOpts.hasMasterPassword && acctDecryptionOpts.keyConnectorOption === undefined;
|
||||
!userDecryptionOpts.hasMasterPassword && userDecryptionOpts.keyConnectorOption === undefined;
|
||||
|
||||
if (requireSetPassword || authResult.resetMasterPassword) {
|
||||
// Change implies going no password -> password in this case
|
||||
|
@ -326,12 +331,12 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
|
|||
private async handleTrustedDeviceEncryptionEnabled(
|
||||
authResult: AuthResult,
|
||||
orgIdentifier: string,
|
||||
acctDecryptionOpts: AccountDecryptionOptions,
|
||||
userDecryptionOpts: UserDecryptionOptions,
|
||||
): Promise<void> {
|
||||
// If user doesn't have a MP, but has reset password permission, they must set a MP
|
||||
if (
|
||||
!acctDecryptionOpts.hasMasterPassword &&
|
||||
acctDecryptionOpts.trustedDeviceOption.hasManageResetPasswordPermission
|
||||
!userDecryptionOpts.hasMasterPassword &&
|
||||
userDecryptionOpts.trustedDeviceOption.hasManageResetPasswordPermission
|
||||
) {
|
||||
// Set flag so that auth guard can redirect to set password screen after decryption (trusted or untrusted device)
|
||||
// Note: we cannot directly navigate to the set password screen in this scenario as we are in a pre-decryption state, and
|
||||
|
|
|
@ -53,7 +53,7 @@ export function lockGuard(): CanActivateFn {
|
|||
|
||||
// User is authN and in locked state.
|
||||
|
||||
const tdeEnabled = await deviceTrustCryptoService.supportsDeviceTrust();
|
||||
const tdeEnabled = await firstValueFrom(deviceTrustCryptoService.supportsDeviceTrust$);
|
||||
|
||||
// Create special exception which allows users to go from the login-initiated page to the lock page for the approve w/ MP flow
|
||||
// The MP check is necessary to prevent direct manual navigation from other locked state pages for users who don't have a MP
|
||||
|
|
|
@ -46,7 +46,7 @@ export function redirectGuard(overrides: Partial<RedirectRoutes> = {}): CanActiv
|
|||
|
||||
// If locked, TDE is enabled, and the user hasn't decrypted yet, then redirect to the
|
||||
// login decryption options component.
|
||||
const tdeEnabled = await deviceTrustCryptoService.supportsDeviceTrust();
|
||||
const tdeEnabled = await firstValueFrom(deviceTrustCryptoService.supportsDeviceTrust$);
|
||||
const everHadUserKey = await firstValueFrom(cryptoService.everHadUserKey$);
|
||||
if (authStatus === AuthenticationStatus.Locked && tdeEnabled && !everHadUserKey) {
|
||||
return router.createUrlTree([routes.notDecrypted], { queryParams: route.queryParams });
|
||||
|
|
|
@ -26,7 +26,7 @@ export function tdeDecryptionRequiredGuard(): CanActivateFn {
|
|||
const router = inject(Router);
|
||||
|
||||
const authStatus = await authService.getAuthStatus();
|
||||
const tdeEnabled = await deviceTrustCryptoService.supportsDeviceTrust();
|
||||
const tdeEnabled = await firstValueFrom(deviceTrustCryptoService.supportsDeviceTrust$);
|
||||
const everHadUserKey = await firstValueFrom(cryptoService.everHadUserKey$);
|
||||
if (authStatus !== AuthenticationStatus.Locked || !tdeEnabled || everHadUserKey) {
|
||||
return router.createUrlTree(["/"]);
|
||||
|
|
|
@ -8,6 +8,9 @@ import {
|
|||
PinCryptoService,
|
||||
LoginStrategyServiceAbstraction,
|
||||
LoginStrategyService,
|
||||
InternalUserDecryptionOptionsServiceAbstraction,
|
||||
UserDecryptionOptionsService,
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service";
|
||||
|
@ -243,8 +246,8 @@ import { safeProvider, SafeProvider } from "../platform/utils/safe-provider";
|
|||
import {
|
||||
LOCALES_DIRECTORY,
|
||||
LOCKED_CALLBACK,
|
||||
LOG_MAC_FAILURES,
|
||||
LOGOUT_CALLBACK,
|
||||
LOG_MAC_FAILURES,
|
||||
MEMORY_STORAGE,
|
||||
OBSERVABLE_DISK_STORAGE,
|
||||
OBSERVABLE_MEMORY_STORAGE,
|
||||
|
@ -369,6 +372,7 @@ const typesafeProviders: Array<SafeProvider> = [
|
|||
PolicyServiceAbstraction,
|
||||
DeviceTrustCryptoServiceAbstraction,
|
||||
AuthRequestServiceAbstraction,
|
||||
InternalUserDecryptionOptionsServiceAbstraction,
|
||||
GlobalStateProvider,
|
||||
BillingAccountProfileStateService,
|
||||
],
|
||||
|
@ -477,6 +481,15 @@ const typesafeProviders: Array<SafeProvider> = [
|
|||
useClass: EnvironmentService,
|
||||
deps: [StateProvider, AccountServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: InternalUserDecryptionOptionsServiceAbstraction,
|
||||
useClass: UserDecryptionOptionsService,
|
||||
deps: [StateProvider],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: UserDecryptionOptionsServiceAbstraction,
|
||||
useExisting: InternalUserDecryptionOptionsServiceAbstraction,
|
||||
}),
|
||||
safeProvider({
|
||||
provide: TotpServiceAbstraction,
|
||||
useClass: TotpService,
|
||||
|
@ -577,6 +590,7 @@ const typesafeProviders: Array<SafeProvider> = [
|
|||
FolderApiServiceAbstraction,
|
||||
InternalOrganizationServiceAbstraction,
|
||||
SendApiServiceAbstraction,
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
AvatarServiceAbstraction,
|
||||
LOGOUT_CALLBACK,
|
||||
BillingAccountProfileStateService,
|
||||
|
@ -587,6 +601,7 @@ const typesafeProviders: Array<SafeProvider> = [
|
|||
provide: VaultTimeoutSettingsServiceAbstraction,
|
||||
useClass: VaultTimeoutSettingsService,
|
||||
deps: [
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
CryptoServiceAbstraction,
|
||||
TokenServiceAbstraction,
|
||||
PolicyServiceAbstraction,
|
||||
|
@ -765,6 +780,7 @@ const typesafeProviders: Array<SafeProvider> = [
|
|||
CryptoServiceAbstraction,
|
||||
I18nServiceAbstraction,
|
||||
UserVerificationApiServiceAbstraction,
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
PinCryptoServiceAbstraction,
|
||||
LogService,
|
||||
VaultTimeoutSettingsServiceAbstraction,
|
||||
|
@ -902,6 +918,7 @@ const typesafeProviders: Array<SafeProvider> = [
|
|||
DevicesApiServiceAbstraction,
|
||||
I18nServiceAbstraction,
|
||||
PlatformUtilsServiceAbstraction,
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
export * from "./pin-crypto.service.abstraction";
|
||||
export * from "./login-strategy.service";
|
||||
export * from "./user-decryption-options.service.abstraction";
|
||||
export * from "./auth-request.service.abstraction";
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
import { Observable } from "rxjs";
|
||||
|
||||
import { UserDecryptionOptions } from "../models";
|
||||
|
||||
export abstract class UserDecryptionOptionsServiceAbstraction {
|
||||
/**
|
||||
* Returns what decryption options are available for the current user.
|
||||
* @remark This is sent from the server on authentication.
|
||||
*/
|
||||
abstract userDecryptionOptions$: Observable<UserDecryptionOptions>;
|
||||
/**
|
||||
* Uses user decryption options to determine if current user has a master password.
|
||||
* @remark This is sent from the server, and does not indicate if the master password
|
||||
* was used to login and/or if a master key is saved locally.
|
||||
*/
|
||||
abstract hasMasterPassword$: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* Returns the user decryption options for the given user id.
|
||||
* @param userId The user id to check.
|
||||
*/
|
||||
abstract userDecryptionOptionsById$(userId: string): Observable<UserDecryptionOptions>;
|
||||
}
|
||||
|
||||
export abstract class InternalUserDecryptionOptionsServiceAbstraction extends UserDecryptionOptionsServiceAbstraction {
|
||||
/**
|
||||
* Sets the current decryption options for the user, contains the current configuration
|
||||
* of the users account related to how they can decrypt their vault.
|
||||
* @remark Intended to be used when user decryption options are received from server, does
|
||||
* not update the server. Consider syncing instead of updating locally.
|
||||
* @param userDecryptionOptions Current user decryption options received from server.
|
||||
*/
|
||||
abstract setUserDecryptionOptions(userDecryptionOptions: UserDecryptionOptions): Promise<void>;
|
||||
}
|
|
@ -17,6 +17,7 @@ import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/sym
|
|||
import { CsprngArray } from "@bitwarden/common/types/csprng";
|
||||
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
|
||||
|
||||
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
|
||||
import { AuthRequestLoginCredentials } from "../models/domain/login-credentials";
|
||||
|
||||
import {
|
||||
|
@ -37,6 +38,7 @@ describe("AuthRequestLoginStrategy", () => {
|
|||
let logService: MockProxy<LogService>;
|
||||
let stateService: MockProxy<StateService>;
|
||||
let twoFactorService: MockProxy<TwoFactorService>;
|
||||
let userDecryptionOptions: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>;
|
||||
let deviceTrustCryptoService: MockProxy<DeviceTrustCryptoServiceAbstraction>;
|
||||
let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;
|
||||
|
||||
|
@ -65,6 +67,7 @@ describe("AuthRequestLoginStrategy", () => {
|
|||
logService = mock<LogService>();
|
||||
stateService = mock<StateService>();
|
||||
twoFactorService = mock<TwoFactorService>();
|
||||
userDecryptionOptions = mock<InternalUserDecryptionOptionsServiceAbstraction>();
|
||||
deviceTrustCryptoService = mock<DeviceTrustCryptoServiceAbstraction>();
|
||||
billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
|
||||
|
||||
|
@ -83,6 +86,7 @@ describe("AuthRequestLoginStrategy", () => {
|
|||
logService,
|
||||
stateService,
|
||||
twoFactorService,
|
||||
userDecryptionOptions,
|
||||
deviceTrustCryptoService,
|
||||
billingAccountProfileStateService,
|
||||
);
|
||||
|
|
|
@ -17,6 +17,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
|
|||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
|
||||
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
|
||||
import { AuthRequestLoginCredentials } from "../models/domain/login-credentials";
|
||||
import { CacheData } from "../services/login-strategies/login-strategy.state";
|
||||
|
||||
|
@ -54,6 +55,7 @@ export class AuthRequestLoginStrategy extends LoginStrategy {
|
|||
logService: LogService,
|
||||
stateService: StateService,
|
||||
twoFactorService: TwoFactorService,
|
||||
userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
|
||||
private deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
|
||||
billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
) {
|
||||
|
@ -67,6 +69,7 @@ export class AuthRequestLoginStrategy extends LoginStrategy {
|
|||
logService,
|
||||
stateService,
|
||||
twoFactorService,
|
||||
userDecryptionOptionsService,
|
||||
billingAccountProfileStateService,
|
||||
);
|
||||
|
||||
|
|
|
@ -28,7 +28,6 @@ import {
|
|||
AccountProfile,
|
||||
AccountTokens,
|
||||
AccountKeys,
|
||||
AccountDecryptionOptions,
|
||||
} from "@bitwarden/common/platform/models/domain/account";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
|
@ -39,8 +38,10 @@ import {
|
|||
import { CsprngArray } from "@bitwarden/common/types/csprng";
|
||||
import { UserKey, MasterKey, DeviceKey } from "@bitwarden/common/types/key";
|
||||
|
||||
import { LoginStrategyServiceAbstraction } from "../abstractions/login-strategy.service";
|
||||
import { PasswordLoginCredentials } from "../models/domain/login-credentials";
|
||||
import { LoginStrategyServiceAbstraction } from "../abstractions";
|
||||
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
|
||||
import { PasswordLoginCredentials } from "../models";
|
||||
import { UserDecryptionOptions } from "../models/domain/user-decryption-options";
|
||||
|
||||
import { PasswordLoginStrategy, PasswordLoginStrategyData } from "./password-login.strategy";
|
||||
|
||||
|
@ -108,6 +109,7 @@ describe("LoginStrategy", () => {
|
|||
let logService: MockProxy<LogService>;
|
||||
let stateService: MockProxy<StateService>;
|
||||
let twoFactorService: MockProxy<TwoFactorService>;
|
||||
let userDecryptionOptionsService: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>;
|
||||
let policyService: MockProxy<PolicyService>;
|
||||
let passwordStrengthService: MockProxy<PasswordStrengthServiceAbstraction>;
|
||||
let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;
|
||||
|
@ -126,7 +128,7 @@ describe("LoginStrategy", () => {
|
|||
logService = mock<LogService>();
|
||||
stateService = mock<StateService>();
|
||||
twoFactorService = mock<TwoFactorService>();
|
||||
|
||||
userDecryptionOptionsService = mock<InternalUserDecryptionOptionsServiceAbstraction>();
|
||||
policyService = mock<PolicyService>();
|
||||
passwordStrengthService = mock<PasswordStrengthService>();
|
||||
billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
|
||||
|
@ -146,6 +148,7 @@ describe("LoginStrategy", () => {
|
|||
logService,
|
||||
stateService,
|
||||
twoFactorService,
|
||||
userDecryptionOptionsService,
|
||||
passwordStrengthService,
|
||||
policyService,
|
||||
loginStrategyService,
|
||||
|
@ -204,9 +207,11 @@ describe("LoginStrategy", () => {
|
|||
...new AccountTokens(),
|
||||
},
|
||||
keys: new AccountKeys(),
|
||||
decryptionOptions: AccountDecryptionOptions.fromResponse(idTokenResponse),
|
||||
}),
|
||||
);
|
||||
expect(userDecryptionOptionsService.setUserDecryptionOptions).toHaveBeenCalledWith(
|
||||
UserDecryptionOptions.fromResponse(idTokenResponse),
|
||||
);
|
||||
expect(messagingService.send).toHaveBeenCalledWith("loggedIn");
|
||||
});
|
||||
|
||||
|
@ -409,6 +414,7 @@ describe("LoginStrategy", () => {
|
|||
logService,
|
||||
stateService,
|
||||
twoFactorService,
|
||||
userDecryptionOptionsService,
|
||||
passwordStrengthService,
|
||||
policyService,
|
||||
loginStrategyService,
|
||||
|
|
|
@ -30,9 +30,9 @@ import {
|
|||
Account,
|
||||
AccountProfile,
|
||||
AccountTokens,
|
||||
AccountDecryptionOptions,
|
||||
} from "@bitwarden/common/platform/models/domain/account";
|
||||
|
||||
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
|
||||
import {
|
||||
UserApiLoginCredentials,
|
||||
PasswordLoginCredentials,
|
||||
|
@ -40,6 +40,7 @@ import {
|
|||
AuthRequestLoginCredentials,
|
||||
WebAuthnLoginCredentials,
|
||||
} from "../models/domain/login-credentials";
|
||||
import { UserDecryptionOptions } from "../models/domain/user-decryption-options";
|
||||
import { CacheData } from "../services/login-strategies/login-strategy.state";
|
||||
|
||||
type IdentityResponse = IdentityTokenResponse | IdentityTwoFactorResponse | IdentityCaptchaResponse;
|
||||
|
@ -69,6 +70,7 @@ export abstract class LoginStrategy {
|
|||
protected logService: LogService,
|
||||
protected stateService: StateService,
|
||||
protected twoFactorService: TwoFactorService,
|
||||
protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
|
||||
protected billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
) {}
|
||||
|
||||
|
@ -203,11 +205,14 @@ export abstract class LoginStrategy {
|
|||
...new AccountTokens(),
|
||||
},
|
||||
keys: accountKeys,
|
||||
decryptionOptions: AccountDecryptionOptions.fromResponse(tokenResponse),
|
||||
adminAuthRequest: adminAuthRequest?.toJSON(),
|
||||
}),
|
||||
);
|
||||
|
||||
await this.userDecryptionOptionsService.setUserDecryptionOptions(
|
||||
UserDecryptionOptions.fromResponse(tokenResponse),
|
||||
);
|
||||
|
||||
await this.billingAccountProfileStateService.setHasPremium(accountInformation.premium, false);
|
||||
}
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@ import { CsprngArray } from "@bitwarden/common/types/csprng";
|
|||
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
|
||||
|
||||
import { LoginStrategyServiceAbstraction } from "../abstractions";
|
||||
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
|
||||
import { PasswordLoginCredentials } from "../models/domain/login-credentials";
|
||||
|
||||
import { identityTokenResponseFactory } from "./login.strategy.spec";
|
||||
|
@ -60,6 +61,7 @@ describe("PasswordLoginStrategy", () => {
|
|||
let logService: MockProxy<LogService>;
|
||||
let stateService: MockProxy<StateService>;
|
||||
let twoFactorService: MockProxy<TwoFactorService>;
|
||||
let userDecryptionOptionsService: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>;
|
||||
let policyService: MockProxy<PolicyService>;
|
||||
let passwordStrengthService: MockProxy<PasswordStrengthServiceAbstraction>;
|
||||
let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;
|
||||
|
@ -79,6 +81,7 @@ describe("PasswordLoginStrategy", () => {
|
|||
logService = mock<LogService>();
|
||||
stateService = mock<StateService>();
|
||||
twoFactorService = mock<TwoFactorService>();
|
||||
userDecryptionOptionsService = mock<InternalUserDecryptionOptionsServiceAbstraction>();
|
||||
policyService = mock<PolicyService>();
|
||||
passwordStrengthService = mock<PasswordStrengthService>();
|
||||
billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
|
||||
|
@ -108,6 +111,7 @@ describe("PasswordLoginStrategy", () => {
|
|||
logService,
|
||||
stateService,
|
||||
twoFactorService,
|
||||
userDecryptionOptionsService,
|
||||
passwordStrengthService,
|
||||
policyService,
|
||||
loginStrategyService,
|
||||
|
|
|
@ -26,6 +26,7 @@ import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/pass
|
|||
import { MasterKey } from "@bitwarden/common/types/key";
|
||||
|
||||
import { LoginStrategyServiceAbstraction } from "../abstractions";
|
||||
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
|
||||
import { PasswordLoginCredentials } from "../models/domain/login-credentials";
|
||||
import { CacheData } from "../services/login-strategies/login-strategy.state";
|
||||
|
||||
|
@ -84,6 +85,7 @@ export class PasswordLoginStrategy extends LoginStrategy {
|
|||
logService: LogService,
|
||||
protected stateService: StateService,
|
||||
twoFactorService: TwoFactorService,
|
||||
userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
|
||||
private passwordStrengthService: PasswordStrengthServiceAbstraction,
|
||||
private policyService: PolicyService,
|
||||
private loginStrategyService: LoginStrategyServiceAbstraction,
|
||||
|
@ -99,6 +101,7 @@ export class PasswordLoginStrategy extends LoginStrategy {
|
|||
logService,
|
||||
stateService,
|
||||
twoFactorService,
|
||||
userDecryptionOptionsService,
|
||||
billingAccountProfileStateService,
|
||||
);
|
||||
|
||||
|
|
|
@ -23,7 +23,10 @@ import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/sym
|
|||
import { CsprngArray } from "@bitwarden/common/types/csprng";
|
||||
import { DeviceKey, UserKey, MasterKey } from "@bitwarden/common/types/key";
|
||||
|
||||
import { AuthRequestServiceAbstraction } from "../abstractions";
|
||||
import {
|
||||
AuthRequestServiceAbstraction,
|
||||
InternalUserDecryptionOptionsServiceAbstraction,
|
||||
} from "../abstractions";
|
||||
import { SsoLoginCredentials } from "../models/domain/login-credentials";
|
||||
|
||||
import { identityTokenResponseFactory } from "./login.strategy.spec";
|
||||
|
@ -39,6 +42,7 @@ describe("SsoLoginStrategy", () => {
|
|||
let logService: MockProxy<LogService>;
|
||||
let stateService: MockProxy<StateService>;
|
||||
let twoFactorService: MockProxy<TwoFactorService>;
|
||||
let userDecryptionOptionsService: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>;
|
||||
let keyConnectorService: MockProxy<KeyConnectorService>;
|
||||
let deviceTrustCryptoService: MockProxy<DeviceTrustCryptoServiceAbstraction>;
|
||||
let authRequestService: MockProxy<AuthRequestServiceAbstraction>;
|
||||
|
@ -66,6 +70,7 @@ describe("SsoLoginStrategy", () => {
|
|||
logService = mock<LogService>();
|
||||
stateService = mock<StateService>();
|
||||
twoFactorService = mock<TwoFactorService>();
|
||||
userDecryptionOptionsService = mock<InternalUserDecryptionOptionsServiceAbstraction>();
|
||||
keyConnectorService = mock<KeyConnectorService>();
|
||||
deviceTrustCryptoService = mock<DeviceTrustCryptoServiceAbstraction>();
|
||||
authRequestService = mock<AuthRequestServiceAbstraction>();
|
||||
|
@ -87,6 +92,7 @@ describe("SsoLoginStrategy", () => {
|
|||
logService,
|
||||
stateService,
|
||||
twoFactorService,
|
||||
userDecryptionOptionsService,
|
||||
keyConnectorService,
|
||||
deviceTrustCryptoService,
|
||||
authRequestService,
|
||||
|
|
|
@ -21,7 +21,10 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
|
|||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
|
||||
import { AuthRequestServiceAbstraction } from "../abstractions";
|
||||
import {
|
||||
InternalUserDecryptionOptionsServiceAbstraction,
|
||||
AuthRequestServiceAbstraction,
|
||||
} from "../abstractions";
|
||||
import { SsoLoginCredentials } from "../models/domain/login-credentials";
|
||||
import { CacheData } from "../services/login-strategies/login-strategy.state";
|
||||
|
||||
|
@ -84,6 +87,7 @@ export class SsoLoginStrategy extends LoginStrategy {
|
|||
logService: LogService,
|
||||
stateService: StateService,
|
||||
twoFactorService: TwoFactorService,
|
||||
userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
|
||||
private keyConnectorService: KeyConnectorService,
|
||||
private deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
|
||||
private authRequestService: AuthRequestServiceAbstraction,
|
||||
|
@ -100,6 +104,7 @@ export class SsoLoginStrategy extends LoginStrategy {
|
|||
logService,
|
||||
stateService,
|
||||
twoFactorService,
|
||||
userDecryptionOptionsService,
|
||||
billingAccountProfileStateService,
|
||||
);
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/sym
|
|||
import { CsprngArray } from "@bitwarden/common/types/csprng";
|
||||
import { UserKey, MasterKey } from "@bitwarden/common/types/key";
|
||||
|
||||
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
|
||||
import { UserApiLoginCredentials } from "../models/domain/login-credentials";
|
||||
|
||||
import { identityTokenResponseFactory } from "./login.strategy.spec";
|
||||
|
@ -35,6 +36,7 @@ describe("UserApiLoginStrategy", () => {
|
|||
let logService: MockProxy<LogService>;
|
||||
let stateService: MockProxy<StateService>;
|
||||
let twoFactorService: MockProxy<TwoFactorService>;
|
||||
let userDecryptionOptionsService: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>;
|
||||
let keyConnectorService: MockProxy<KeyConnectorService>;
|
||||
let environmentService: MockProxy<EnvironmentService>;
|
||||
let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;
|
||||
|
@ -57,6 +59,7 @@ describe("UserApiLoginStrategy", () => {
|
|||
logService = mock<LogService>();
|
||||
stateService = mock<StateService>();
|
||||
twoFactorService = mock<TwoFactorService>();
|
||||
userDecryptionOptionsService = mock<InternalUserDecryptionOptionsServiceAbstraction>();
|
||||
keyConnectorService = mock<KeyConnectorService>();
|
||||
environmentService = mock<EnvironmentService>();
|
||||
billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
|
||||
|
@ -76,6 +79,7 @@ describe("UserApiLoginStrategy", () => {
|
|||
logService,
|
||||
stateService,
|
||||
twoFactorService,
|
||||
userDecryptionOptionsService,
|
||||
environmentService,
|
||||
keyConnectorService,
|
||||
billingAccountProfileStateService,
|
||||
|
|
|
@ -17,6 +17,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
|
|||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
|
||||
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
|
||||
import { UserApiLoginCredentials } from "../models/domain/login-credentials";
|
||||
import { CacheData } from "../services/login-strategies/login-strategy.state";
|
||||
|
||||
|
@ -47,6 +48,7 @@ export class UserApiLoginStrategy extends LoginStrategy {
|
|||
logService: LogService,
|
||||
stateService: StateService,
|
||||
twoFactorService: TwoFactorService,
|
||||
userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
|
||||
private environmentService: EnvironmentService,
|
||||
private keyConnectorService: KeyConnectorService,
|
||||
billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
|
@ -61,6 +63,7 @@ export class UserApiLoginStrategy extends LoginStrategy {
|
|||
logService,
|
||||
stateService,
|
||||
twoFactorService,
|
||||
userDecryptionOptionsService,
|
||||
billingAccountProfileStateService,
|
||||
);
|
||||
this.cache = new BehaviorSubject(data);
|
||||
|
|
|
@ -18,6 +18,7 @@ import { Utils } from "@bitwarden/common/platform/misc/utils";
|
|||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { PrfKey, UserKey } from "@bitwarden/common/types/key";
|
||||
|
||||
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
|
||||
import { WebAuthnLoginCredentials } from "../models/domain/login-credentials";
|
||||
|
||||
import { identityTokenResponseFactory } from "./login.strategy.spec";
|
||||
|
@ -35,6 +36,7 @@ describe("WebAuthnLoginStrategy", () => {
|
|||
let logService!: MockProxy<LogService>;
|
||||
let stateService!: MockProxy<StateService>;
|
||||
let twoFactorService!: MockProxy<TwoFactorService>;
|
||||
let userDecryptionOptionsService: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>;
|
||||
let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;
|
||||
|
||||
let webAuthnLoginStrategy!: WebAuthnLoginStrategy;
|
||||
|
@ -70,6 +72,7 @@ describe("WebAuthnLoginStrategy", () => {
|
|||
logService = mock<LogService>();
|
||||
stateService = mock<StateService>();
|
||||
twoFactorService = mock<TwoFactorService>();
|
||||
userDecryptionOptionsService = mock<InternalUserDecryptionOptionsServiceAbstraction>();
|
||||
billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
|
||||
|
||||
tokenService.getTwoFactorToken.mockResolvedValue(null);
|
||||
|
@ -87,6 +90,7 @@ describe("WebAuthnLoginStrategy", () => {
|
|||
logService,
|
||||
stateService,
|
||||
twoFactorService,
|
||||
userDecryptionOptionsService,
|
||||
billingAccountProfileStateService,
|
||||
);
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv
|
|||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { UserKey } from "@bitwarden/common/types/key";
|
||||
|
||||
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions";
|
||||
import { WebAuthnLoginCredentials } from "../models/domain/login-credentials";
|
||||
import { CacheData } from "../services/login-strategies/login-strategy.state";
|
||||
|
||||
|
@ -49,6 +50,7 @@ export class WebAuthnLoginStrategy extends LoginStrategy {
|
|||
logService: LogService,
|
||||
stateService: StateService,
|
||||
twoFactorService: TwoFactorService,
|
||||
userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
|
||||
billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
) {
|
||||
super(
|
||||
|
@ -61,6 +63,7 @@ export class WebAuthnLoginStrategy extends LoginStrategy {
|
|||
logService,
|
||||
stateService,
|
||||
twoFactorService,
|
||||
userDecryptionOptionsService,
|
||||
billingAccountProfileStateService,
|
||||
);
|
||||
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
export * from "./rotateable-key-set";
|
||||
export * from "./login-credentials";
|
||||
export * from "./user-decryption-options";
|
||||
|
|
|
@ -0,0 +1,153 @@
|
|||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { KeyConnectorUserDecryptionOptionResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/key-connector-user-decryption-option.response";
|
||||
import { TrustedDeviceUserDecryptionOptionResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/trusted-device-user-decryption-option.response";
|
||||
import { IdentityTokenResponse } from "@bitwarden/common/src/auth/models/response/identity-token.response";
|
||||
|
||||
/**
|
||||
* Key Connector decryption options. Intended to be sent to the client for use after authentication.
|
||||
* @see {@link UserDecryptionOptions}
|
||||
*/
|
||||
export class KeyConnectorUserDecryptionOption {
|
||||
/** The URL of the key connector configured for this user. */
|
||||
keyConnectorUrl: string;
|
||||
|
||||
/**
|
||||
* Initializes a new instance of the KeyConnectorUserDecryptionOption from a response object.
|
||||
* @param response The key connector user decryption option response object.
|
||||
* @returns A new instance of the KeyConnectorUserDecryptionOption. Will initialize even if the response is nullish.
|
||||
*/
|
||||
static fromResponse(
|
||||
response: KeyConnectorUserDecryptionOptionResponse,
|
||||
): KeyConnectorUserDecryptionOption {
|
||||
const options = new KeyConnectorUserDecryptionOption();
|
||||
options.keyConnectorUrl = response?.keyConnectorUrl ?? null;
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a new instance of a KeyConnectorUserDecryptionOption from a JSON object.
|
||||
* @param obj JSON object to deserialize.
|
||||
* @returns A new instance of the KeyConnectorUserDecryptionOption. Will initialize even if the JSON object is nullish.
|
||||
*/
|
||||
static fromJSON(
|
||||
obj: Jsonify<KeyConnectorUserDecryptionOption>,
|
||||
): KeyConnectorUserDecryptionOption {
|
||||
return Object.assign(new KeyConnectorUserDecryptionOption(), obj);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trusted device decryption options. Intended to be sent to the client for use after authentication.
|
||||
* @see {@link UserDecryptionOptions}
|
||||
*/
|
||||
export class TrustedDeviceUserDecryptionOption {
|
||||
/** True if an admin has approved an admin auth request previously made from this device. */
|
||||
hasAdminApproval: boolean;
|
||||
/** True if the user has a device capable of approving an auth request. */
|
||||
hasLoginApprovingDevice: boolean;
|
||||
/** True if the user has manage reset password permission, as these users must be forced to have a master password. */
|
||||
hasManageResetPasswordPermission: boolean;
|
||||
|
||||
/**
|
||||
* Initializes a new instance of the TrustedDeviceUserDecryptionOption from a response object.
|
||||
* @param response The trusted device user decryption option response object.
|
||||
* @returns A new instance of the TrustedDeviceUserDecryptionOption. Will initialize even if the response is nullish.
|
||||
*/
|
||||
static fromResponse(
|
||||
response: TrustedDeviceUserDecryptionOptionResponse,
|
||||
): TrustedDeviceUserDecryptionOption {
|
||||
const options = new TrustedDeviceUserDecryptionOption();
|
||||
options.hasAdminApproval = response?.hasAdminApproval ?? false;
|
||||
options.hasLoginApprovingDevice = response?.hasLoginApprovingDevice ?? false;
|
||||
options.hasManageResetPasswordPermission = response?.hasManageResetPasswordPermission ?? false;
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a new instance of the TrustedDeviceUserDecryptionOption from a JSON object.
|
||||
* @param obj JSON object to deserialize.
|
||||
* @returns A new instance of the TrustedDeviceUserDecryptionOption. Will initialize even if the JSON object is nullish.
|
||||
*/
|
||||
static fromJSON(
|
||||
obj: Jsonify<TrustedDeviceUserDecryptionOption>,
|
||||
): TrustedDeviceUserDecryptionOption {
|
||||
return Object.assign(new TrustedDeviceUserDecryptionOption(), obj);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the decryption options the user has configured on the server. This is intended to be sent
|
||||
* to the client on authentication, and can be used to determine how to decrypt the user's vault.
|
||||
*/
|
||||
export class UserDecryptionOptions {
|
||||
/** True if the user has a master password configured on the server. */
|
||||
hasMasterPassword: boolean;
|
||||
/** {@link TrustedDeviceUserDecryptionOption} */
|
||||
trustedDeviceOption?: TrustedDeviceUserDecryptionOption;
|
||||
/** {@link KeyConnectorUserDecryptionOption} */
|
||||
keyConnectorOption?: KeyConnectorUserDecryptionOption;
|
||||
|
||||
/**
|
||||
* Initializes a new instance of the UserDecryptionOptions from a response object.
|
||||
* @param response user decryption options response object
|
||||
* @returns A new instance of the UserDecryptionOptions.
|
||||
* @throws If the response is nullish, this method will throw an error. User decryption options
|
||||
* are required for client initialization.
|
||||
*/
|
||||
// TODO: Change response type to `UserDecryptionOptionsResponse` after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3537)
|
||||
static fromResponse(response: IdentityTokenResponse): UserDecryptionOptions {
|
||||
if (response == null) {
|
||||
throw new Error("User Decryption Options are required for client initialization.");
|
||||
}
|
||||
|
||||
const decryptionOptions = new UserDecryptionOptions();
|
||||
|
||||
if (response.userDecryptionOptions) {
|
||||
// If the response has userDecryptionOptions, this means it's on a post-TDE server version and can interrogate
|
||||
// the new decryption options.
|
||||
const responseOptions = response.userDecryptionOptions;
|
||||
decryptionOptions.hasMasterPassword = responseOptions.hasMasterPassword;
|
||||
|
||||
decryptionOptions.trustedDeviceOption = TrustedDeviceUserDecryptionOption.fromResponse(
|
||||
responseOptions.trustedDeviceOption,
|
||||
);
|
||||
|
||||
decryptionOptions.keyConnectorOption = KeyConnectorUserDecryptionOption.fromResponse(
|
||||
responseOptions.keyConnectorOption,
|
||||
);
|
||||
} else {
|
||||
// If the response does not have userDecryptionOptions, this means it's on a pre-TDE server version and so
|
||||
// we must base our decryption options on the presence of the keyConnectorUrl.
|
||||
// Note that the presence of keyConnectorUrl implies that the user does not have a master password, as in pre-TDE
|
||||
// server versions, a master password short-circuited the addition of the keyConnectorUrl to the response.
|
||||
// TODO: remove this check after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3537)
|
||||
const usingKeyConnector = response.keyConnectorUrl != null;
|
||||
decryptionOptions.hasMasterPassword = !usingKeyConnector;
|
||||
if (usingKeyConnector) {
|
||||
decryptionOptions.keyConnectorOption = new KeyConnectorUserDecryptionOption();
|
||||
decryptionOptions.keyConnectorOption.keyConnectorUrl = response.keyConnectorUrl;
|
||||
}
|
||||
}
|
||||
return decryptionOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a new instance of the UserDecryptionOptions from a JSON object.
|
||||
* @param obj JSON object to deserialize.
|
||||
* @returns A new instance of the UserDecryptionOptions. Will initialize even if the JSON object is nullish.
|
||||
*/
|
||||
static fromJSON(obj: Jsonify<UserDecryptionOptions>): UserDecryptionOptions {
|
||||
const decryptionOptions = Object.assign(new UserDecryptionOptions(), obj);
|
||||
|
||||
decryptionOptions.trustedDeviceOption = TrustedDeviceUserDecryptionOption.fromJSON(
|
||||
obj?.trustedDeviceOption,
|
||||
);
|
||||
|
||||
decryptionOptions.keyConnectorOption = KeyConnectorUserDecryptionOption.fromJSON(
|
||||
obj?.keyConnectorOption,
|
||||
);
|
||||
|
||||
return decryptionOptions;
|
||||
}
|
||||
}
|
|
@ -1 +1,2 @@
|
|||
export * from "./domain";
|
||||
export * from "./spec";
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
import {
|
||||
KeyConnectorUserDecryptionOption,
|
||||
TrustedDeviceUserDecryptionOption,
|
||||
UserDecryptionOptions,
|
||||
} from "../domain";
|
||||
|
||||
// To discourage creating new user decryption options, we don't expose a constructor.
|
||||
// These helpers are for testing purposes only.
|
||||
|
||||
/** Testing helper for creating new instances of `UserDecryptionOptions` */
|
||||
export class FakeUserDecryptionOptions extends UserDecryptionOptions {
|
||||
constructor(init: Partial<UserDecryptionOptions>) {
|
||||
super();
|
||||
Object.assign(this, init);
|
||||
}
|
||||
}
|
||||
|
||||
/** Testing helper for creating new instances of `KeyConnectorUserDecryptionOption` */
|
||||
export class FakeKeyConnectorUserDecryptionOption extends KeyConnectorUserDecryptionOption {
|
||||
constructor(keyConnectorUrl: string) {
|
||||
super();
|
||||
this.keyConnectorUrl = keyConnectorUrl;
|
||||
}
|
||||
}
|
||||
|
||||
/** Testing helper for creating new instances of `TrustedDeviceUserDecryptionOption` */
|
||||
export class FakeTrustedDeviceUserDecryptionOption extends TrustedDeviceUserDecryptionOption {
|
||||
constructor(
|
||||
hasAdminApproval: boolean,
|
||||
hasLoginApprovingDevice: boolean,
|
||||
hasManageResetPasswordPermission: boolean,
|
||||
) {
|
||||
super();
|
||||
this.hasAdminApproval = hasAdminApproval;
|
||||
this.hasLoginApprovingDevice = hasLoginApprovingDevice;
|
||||
this.hasManageResetPasswordPermission = hasManageResetPasswordPermission;
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export * from "./fake-user-decryption-options";
|
|
@ -1,3 +1,4 @@
|
|||
export * from "./pin-crypto/pin-crypto.service.implementation";
|
||||
export * from "./login-strategies/login-strategy.service";
|
||||
export * from "./user-decryption-options/user-decryption-options.service";
|
||||
export * from "./auth-request/auth-request.service";
|
||||
|
|
|
@ -25,8 +25,12 @@ import { KdfType } from "@bitwarden/common/platform/enums";
|
|||
import { FakeGlobalState, FakeGlobalStateProvider } from "@bitwarden/common/spec";
|
||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||
|
||||
import { AuthRequestServiceAbstraction } from "../../abstractions";
|
||||
import {
|
||||
AuthRequestServiceAbstraction,
|
||||
InternalUserDecryptionOptionsServiceAbstraction,
|
||||
} from "../../abstractions";
|
||||
import { PasswordLoginCredentials } from "../../models";
|
||||
import { UserDecryptionOptionsService } from "../user-decryption-options/user-decryption-options.service";
|
||||
|
||||
import { LoginStrategyService } from "./login-strategy.service";
|
||||
import { CACHE_EXPIRATION_KEY } from "./login-strategy.state";
|
||||
|
@ -51,6 +55,7 @@ describe("LoginStrategyService", () => {
|
|||
let policyService: MockProxy<PolicyService>;
|
||||
let deviceTrustCryptoService: MockProxy<DeviceTrustCryptoServiceAbstraction>;
|
||||
let authRequestService: MockProxy<AuthRequestServiceAbstraction>;
|
||||
let userDecryptionOptionsService: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>;
|
||||
let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;
|
||||
|
||||
let stateProvider: FakeGlobalStateProvider;
|
||||
|
@ -74,6 +79,7 @@ describe("LoginStrategyService", () => {
|
|||
policyService = mock<PolicyService>();
|
||||
deviceTrustCryptoService = mock<DeviceTrustCryptoServiceAbstraction>();
|
||||
authRequestService = mock<AuthRequestServiceAbstraction>();
|
||||
userDecryptionOptionsService = mock<UserDecryptionOptionsService>();
|
||||
billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
|
||||
stateProvider = new FakeGlobalStateProvider();
|
||||
|
||||
|
@ -95,6 +101,7 @@ describe("LoginStrategyService", () => {
|
|||
policyService,
|
||||
deviceTrustCryptoService,
|
||||
authRequestService,
|
||||
userDecryptionOptionsService,
|
||||
stateProvider,
|
||||
billingAccountProfileStateService,
|
||||
);
|
||||
|
|
|
@ -40,6 +40,7 @@ import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/pass
|
|||
import { MasterKey } from "@bitwarden/common/types/key";
|
||||
|
||||
import { AuthRequestServiceAbstraction, LoginStrategyServiceAbstraction } from "../../abstractions";
|
||||
import { InternalUserDecryptionOptionsServiceAbstraction } from "../../abstractions/user-decryption-options.service.abstraction";
|
||||
import { AuthRequestLoginStrategy } from "../../login-strategies/auth-request-login.strategy";
|
||||
import { PasswordLoginStrategy } from "../../login-strategies/password-login.strategy";
|
||||
import { SsoLoginStrategy } from "../../login-strategies/sso-login.strategy";
|
||||
|
@ -101,6 +102,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
|
|||
protected policyService: PolicyService,
|
||||
protected deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
|
||||
protected authRequestService: AuthRequestServiceAbstraction,
|
||||
protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
|
||||
protected stateProvider: GlobalStateProvider,
|
||||
protected billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
) {
|
||||
|
@ -354,6 +356,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
|
|||
this.logService,
|
||||
this.stateService,
|
||||
this.twoFactorService,
|
||||
this.userDecryptionOptionsService,
|
||||
this.passwordStrengthService,
|
||||
this.policyService,
|
||||
this,
|
||||
|
@ -371,6 +374,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
|
|||
this.logService,
|
||||
this.stateService,
|
||||
this.twoFactorService,
|
||||
this.userDecryptionOptionsService,
|
||||
this.keyConnectorService,
|
||||
this.deviceTrustCryptoService,
|
||||
this.authRequestService,
|
||||
|
@ -389,6 +393,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
|
|||
this.logService,
|
||||
this.stateService,
|
||||
this.twoFactorService,
|
||||
this.userDecryptionOptionsService,
|
||||
this.environmentService,
|
||||
this.keyConnectorService,
|
||||
this.billingAccountProfileStateService,
|
||||
|
@ -405,6 +410,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
|
|||
this.logService,
|
||||
this.stateService,
|
||||
this.twoFactorService,
|
||||
this.userDecryptionOptionsService,
|
||||
this.deviceTrustCryptoService,
|
||||
this.billingAccountProfileStateService,
|
||||
);
|
||||
|
@ -420,6 +426,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
|
|||
this.logService,
|
||||
this.stateService,
|
||||
this.twoFactorService,
|
||||
this.userDecryptionOptionsService,
|
||||
this.billingAccountProfileStateService,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import {
|
||||
FakeAccountService,
|
||||
FakeStateProvider,
|
||||
mockAccountServiceWith,
|
||||
} from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import {
|
||||
USER_DECRYPTION_OPTIONS,
|
||||
UserDecryptionOptionsService,
|
||||
} from "./user-decryption-options.service";
|
||||
|
||||
describe("UserDecryptionOptionsService", () => {
|
||||
let sut: UserDecryptionOptionsService;
|
||||
|
||||
const fakeUserId = Utils.newGuid() as UserId;
|
||||
let fakeAccountService: FakeAccountService;
|
||||
let fakeStateProvider: FakeStateProvider;
|
||||
|
||||
beforeEach(() => {
|
||||
fakeAccountService = mockAccountServiceWith(fakeUserId);
|
||||
fakeStateProvider = new FakeStateProvider(fakeAccountService);
|
||||
|
||||
sut = new UserDecryptionOptionsService(fakeStateProvider);
|
||||
});
|
||||
|
||||
const userDecryptionOptions = {
|
||||
hasMasterPassword: true,
|
||||
trustedDeviceOption: {
|
||||
hasAdminApproval: false,
|
||||
hasLoginApprovingDevice: false,
|
||||
hasManageResetPasswordPermission: true,
|
||||
},
|
||||
keyConnectorOption: {
|
||||
keyConnectorUrl: "https://keyconnector.bitwarden.com",
|
||||
},
|
||||
};
|
||||
|
||||
describe("userDecryptionOptions$", () => {
|
||||
it("should return the active user's decryption options", async () => {
|
||||
await fakeStateProvider.setUserState(USER_DECRYPTION_OPTIONS, userDecryptionOptions);
|
||||
|
||||
const result = await firstValueFrom(sut.userDecryptionOptions$);
|
||||
|
||||
expect(result).toEqual(userDecryptionOptions);
|
||||
});
|
||||
});
|
||||
|
||||
describe("hasMasterPassword$", () => {
|
||||
it("should return the hasMasterPassword property of the active user's decryption options", async () => {
|
||||
await fakeStateProvider.setUserState(USER_DECRYPTION_OPTIONS, userDecryptionOptions);
|
||||
|
||||
const result = await firstValueFrom(sut.hasMasterPassword$);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("userDecryptionOptionsById$", () => {
|
||||
it("should return the user decryption options for the given user", async () => {
|
||||
const givenUser = Utils.newGuid() as UserId;
|
||||
await fakeAccountService.addAccount(givenUser, {
|
||||
name: "Test User 1",
|
||||
email: "test1@email.com",
|
||||
status: AuthenticationStatus.Locked,
|
||||
});
|
||||
await fakeStateProvider.setUserState(
|
||||
USER_DECRYPTION_OPTIONS,
|
||||
userDecryptionOptions,
|
||||
givenUser,
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(sut.userDecryptionOptionsById$(givenUser));
|
||||
|
||||
expect(result).toEqual(userDecryptionOptions);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setUserDecryptionOptions", () => {
|
||||
it("should set the active user's decryption options", async () => {
|
||||
await sut.setUserDecryptionOptions(userDecryptionOptions);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
fakeStateProvider.getActive(USER_DECRYPTION_OPTIONS).state$,
|
||||
);
|
||||
|
||||
expect(result).toEqual(userDecryptionOptions);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,47 @@
|
|||
import { map } from "rxjs";
|
||||
|
||||
import {
|
||||
ActiveUserState,
|
||||
StateProvider,
|
||||
USER_DECRYPTION_OPTIONS_DISK,
|
||||
UserKeyDefinition,
|
||||
} from "@bitwarden/common/platform/state";
|
||||
import { UserId } from "@bitwarden/common/src/types/guid";
|
||||
|
||||
import { InternalUserDecryptionOptionsServiceAbstraction } from "../../abstractions/user-decryption-options.service.abstraction";
|
||||
import { UserDecryptionOptions } from "../../models";
|
||||
|
||||
export const USER_DECRYPTION_OPTIONS = new UserKeyDefinition<UserDecryptionOptions>(
|
||||
USER_DECRYPTION_OPTIONS_DISK,
|
||||
"decryptionOptions",
|
||||
{
|
||||
deserializer: (decryptionOptions) => UserDecryptionOptions.fromJSON(decryptionOptions),
|
||||
clearOn: ["logout"],
|
||||
},
|
||||
);
|
||||
|
||||
export class UserDecryptionOptionsService
|
||||
implements InternalUserDecryptionOptionsServiceAbstraction
|
||||
{
|
||||
private userDecryptionOptionsState: ActiveUserState<UserDecryptionOptions>;
|
||||
|
||||
userDecryptionOptions$;
|
||||
hasMasterPassword$;
|
||||
|
||||
constructor(private stateProvider: StateProvider) {
|
||||
this.userDecryptionOptionsState = this.stateProvider.getActive(USER_DECRYPTION_OPTIONS);
|
||||
|
||||
this.userDecryptionOptions$ = this.userDecryptionOptionsState.state$;
|
||||
this.hasMasterPassword$ = this.userDecryptionOptions$.pipe(
|
||||
map((options) => options?.hasMasterPassword ?? false),
|
||||
);
|
||||
}
|
||||
|
||||
userDecryptionOptionsById$(userId: UserId) {
|
||||
return this.stateProvider.getUser(userId, USER_DECRYPTION_OPTIONS).state$;
|
||||
}
|
||||
|
||||
async setUserDecryptionOptions(userDecryptionOptions: UserDecryptionOptions): Promise<void> {
|
||||
await this.userDecryptionOptionsState.update((_) => userDecryptionOptions);
|
||||
}
|
||||
}
|
|
@ -1,8 +1,11 @@
|
|||
import { Observable } from "rxjs";
|
||||
|
||||
import { EncString } from "../../platform/models/domain/enc-string";
|
||||
import { DeviceKey, UserKey } from "../../types/key";
|
||||
import { DeviceResponse } from "../abstractions/devices/responses/device.response";
|
||||
|
||||
export abstract class DeviceTrustCryptoServiceAbstraction {
|
||||
supportsDeviceTrust$: Observable<boolean>;
|
||||
/**
|
||||
* @description Retrieves the users choice to trust the device which can only happen after decryption
|
||||
* Note: this value should only be used once and then reset
|
||||
|
@ -20,6 +23,4 @@ export abstract class DeviceTrustCryptoServiceAbstraction {
|
|||
deviceKey?: DeviceKey,
|
||||
) => Promise<UserKey | null>;
|
||||
rotateDevicesTrust: (newUserKey: UserKey, masterPasswordHash: string) => Promise<void>;
|
||||
|
||||
supportsDeviceTrust: () => Promise<boolean>;
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ export class AuthResult {
|
|||
// TODO: PM-3287 - Remove this after 3 releases of backwards compatibility. - Target release 2023.12 for removal
|
||||
/**
|
||||
* @deprecated
|
||||
* Replace with using AccountDecryptionOptions to determine if the user does
|
||||
* Replace with using UserDecryptionOptions to determine if the user does
|
||||
* not have a master password and is not using Key Connector.
|
||||
* */
|
||||
resetMasterPassword = false;
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
export class KeyConnectorUserDecryptionOption {
|
||||
constructor(public keyConnectorUrl: string) {}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
export class TrustedDeviceUserDecryptionOption {
|
||||
constructor(
|
||||
public hasAdminApproval: boolean,
|
||||
public hasLoginApprovingDevice: boolean,
|
||||
public hasManageResetPasswordPermission: boolean,
|
||||
) {}
|
||||
}
|
|
@ -1,4 +1,6 @@
|
|||
import { firstValueFrom } from "rxjs";
|
||||
import { firstValueFrom, map, Observable } from "rxjs";
|
||||
|
||||
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
||||
|
||||
import { AppIdService } from "../../platform/abstractions/app-id.service";
|
||||
import { CryptoFunctionService } from "../../platform/abstractions/crypto-function.service";
|
||||
|
@ -21,6 +23,8 @@ import {
|
|||
} from "../models/request/update-devices-trust.request";
|
||||
|
||||
export class DeviceTrustCryptoService implements DeviceTrustCryptoServiceAbstraction {
|
||||
supportsDeviceTrust$: Observable<boolean>;
|
||||
|
||||
constructor(
|
||||
private keyGenerationService: KeyGenerationService,
|
||||
private cryptoFunctionService: CryptoFunctionService,
|
||||
|
@ -31,7 +35,12 @@ export class DeviceTrustCryptoService implements DeviceTrustCryptoServiceAbstrac
|
|||
private devicesApiService: DevicesApiServiceAbstraction,
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
) {}
|
||||
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
||||
) {
|
||||
this.supportsDeviceTrust$ = this.userDecryptionOptionsService.userDecryptionOptions$.pipe(
|
||||
map((options) => options?.trustedDeviceOption != null ?? false),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Retrieves the users choice to trust the device which can only happen after decryption
|
||||
|
@ -203,9 +212,4 @@ export class DeviceTrustCryptoService implements DeviceTrustCryptoServiceAbstrac
|
|||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async supportsDeviceTrust(): Promise<boolean> {
|
||||
const decryptionOptions = await this.stateService.getAccountDecryptionOptions();
|
||||
return decryptionOptions?.trustedDeviceOption != null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import { matches, mock } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
|
||||
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
||||
|
||||
import { UserDecryptionOptions } from "../../../../auth/src/common/models/domain/user-decryption-options";
|
||||
import { DeviceType } from "../../enums";
|
||||
import { AppIdService } from "../../platform/abstractions/app-id.service";
|
||||
import { CryptoFunctionService } from "../../platform/abstractions/crypto-function.service";
|
||||
|
@ -34,10 +37,16 @@ describe("deviceTrustCryptoService", () => {
|
|||
const devicesApiService = mock<DevicesApiServiceAbstraction>();
|
||||
const i18nService = mock<I18nService>();
|
||||
const platformUtilsService = mock<PlatformUtilsService>();
|
||||
const userDecryptionOptionsService = mock<UserDecryptionOptionsServiceAbstraction>();
|
||||
|
||||
const decryptionOptions = new BehaviorSubject<UserDecryptionOptions>(null);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
decryptionOptions.next({} as any);
|
||||
userDecryptionOptionsService.userDecryptionOptions$ = decryptionOptions;
|
||||
|
||||
deviceTrustCryptoService = new DeviceTrustCryptoService(
|
||||
keyGenerationService,
|
||||
cryptoFunctionService,
|
||||
|
@ -48,6 +57,7 @@ describe("deviceTrustCryptoService", () => {
|
|||
devicesApiService,
|
||||
i18nService,
|
||||
platformUtilsService,
|
||||
userDecryptionOptionsService,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
||||
|
||||
import { PinCryptoServiceAbstraction } from "../../../../../auth/src/common/abstractions/pin-crypto.service.abstraction";
|
||||
import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "../../../abstractions/vault-timeout/vault-timeout-settings.service";
|
||||
import { CryptoService } from "../../../platform/abstractions/crypto.service";
|
||||
|
@ -33,6 +37,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
|
|||
private cryptoService: CryptoService,
|
||||
private i18nService: I18nService,
|
||||
private userVerificationApiService: UserVerificationApiServiceAbstraction,
|
||||
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
||||
private pinCryptoService: PinCryptoServiceAbstraction,
|
||||
private logService: LogService,
|
||||
private vaultTimeoutSettingsService: VaultTimeoutSettingsServiceAbstraction,
|
||||
|
@ -135,7 +140,6 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
|
|||
case VerificationType.MasterPassword:
|
||||
return this.verifyUserByMasterPassword(verification);
|
||||
case VerificationType.PIN:
|
||||
return this.verifyUserByPIN(verification);
|
||||
break;
|
||||
case VerificationType.Biometrics:
|
||||
return this.verifyUserByBiometrics();
|
||||
|
@ -210,16 +214,19 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
|
|||
* Note: This only checks the server, not the local state
|
||||
* @param userId The user id to check. If not provided, the current user is used
|
||||
* @returns True if the user has a master password
|
||||
* @deprecated Use UserDecryptionOptionsService.hasMasterPassword$ instead
|
||||
*/
|
||||
async hasMasterPassword(userId?: string): Promise<boolean> {
|
||||
const decryptionOptions = await this.stateService.getAccountDecryptionOptions({ userId });
|
||||
if (userId) {
|
||||
const decryptionOptions = await firstValueFrom(
|
||||
this.userDecryptionOptionsService.userDecryptionOptionsById$(userId),
|
||||
);
|
||||
|
||||
if (decryptionOptions?.hasMasterPassword != undefined) {
|
||||
return decryptionOptions.hasMasterPassword;
|
||||
}
|
||||
|
||||
// TODO: PM-3518 - Left for backwards compatibility, remove after 2023.12.0
|
||||
return !(await this.stateService.getUsesKeyConnector({ userId }));
|
||||
}
|
||||
return await firstValueFrom(this.userDecryptionOptionsService.hasMasterPassword$);
|
||||
}
|
||||
|
||||
async hasMasterPasswordAndMasterKeyHash(userId?: string): Promise<boolean> {
|
||||
|
|
|
@ -18,7 +18,7 @@ import { CipherView } from "../../vault/models/view/cipher.view";
|
|||
import { AddEditCipherInfo } from "../../vault/types/add-edit-cipher-info";
|
||||
import { KdfType } from "../enums";
|
||||
import { ServerConfigData } from "../models/data/server-config.data";
|
||||
import { Account, AccountDecryptionOptions } from "../models/domain/account";
|
||||
import { Account } from "../models/domain/account";
|
||||
import { EncString } from "../models/domain/enc-string";
|
||||
import { StorageOptions } from "../models/domain/storage-options";
|
||||
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
|
||||
|
@ -180,13 +180,6 @@ export abstract class StateService<T extends Account = Account> {
|
|||
) => Promise<void>;
|
||||
getShouldTrustDevice: (options?: StorageOptions) => Promise<boolean | null>;
|
||||
setShouldTrustDevice: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getAccountDecryptionOptions: (
|
||||
options?: StorageOptions,
|
||||
) => Promise<AccountDecryptionOptions | null>;
|
||||
setAccountDecryptionOptions: (
|
||||
value: AccountDecryptionOptions,
|
||||
options?: StorageOptions,
|
||||
) => Promise<void>;
|
||||
getEmail: (options?: StorageOptions) => Promise<string>;
|
||||
setEmail: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getEmailVerified: (options?: StorageOptions) => Promise<boolean>;
|
||||
|
|
|
@ -2,9 +2,6 @@ import { Jsonify } from "type-fest";
|
|||
|
||||
import { AdminAuthRequestStorable } from "../../../auth/models/domain/admin-auth-req-storable";
|
||||
import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason";
|
||||
import { KeyConnectorUserDecryptionOption } from "../../../auth/models/domain/user-decryption-options/key-connector-user-decryption-option";
|
||||
import { TrustedDeviceUserDecryptionOption } from "../../../auth/models/domain/user-decryption-options/trusted-device-user-decryption-option";
|
||||
import { IdentityTokenResponse } from "../../../auth/models/response/identity-token.response";
|
||||
import { UriMatchStrategySetting } from "../../../models/domain/domain-service";
|
||||
import { GeneratorOptions } from "../../../tools/generator/generator-options";
|
||||
import {
|
||||
|
@ -235,103 +232,12 @@ export class AccountTokens {
|
|||
}
|
||||
}
|
||||
|
||||
export class AccountDecryptionOptions {
|
||||
hasMasterPassword: boolean;
|
||||
trustedDeviceOption?: TrustedDeviceUserDecryptionOption;
|
||||
keyConnectorOption?: KeyConnectorUserDecryptionOption;
|
||||
|
||||
constructor(init?: Partial<AccountDecryptionOptions>) {
|
||||
if (init) {
|
||||
Object.assign(this, init);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: these nice getters don't work because the Account object is not properly being deserialized out of
|
||||
// JSON (the Account static fromJSON method is not running) so these getters don't exist on the
|
||||
// account decryptions options object when pulled out of state. This is a bug that needs to be fixed later on
|
||||
// get hasTrustedDeviceOption(): boolean {
|
||||
// return this.trustedDeviceOption !== null && this.trustedDeviceOption !== undefined;
|
||||
// }
|
||||
|
||||
// get hasKeyConnectorOption(): boolean {
|
||||
// return this.keyConnectorOption !== null && this.keyConnectorOption !== undefined;
|
||||
// }
|
||||
|
||||
static fromResponse(response: IdentityTokenResponse): AccountDecryptionOptions {
|
||||
if (response == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const accountDecryptionOptions = new AccountDecryptionOptions();
|
||||
|
||||
if (response.userDecryptionOptions) {
|
||||
// If the response has userDecryptionOptions, this means it's on a post-TDE server version and can interrogate
|
||||
// the new decryption options.
|
||||
const responseOptions = response.userDecryptionOptions;
|
||||
accountDecryptionOptions.hasMasterPassword = responseOptions.hasMasterPassword;
|
||||
|
||||
if (responseOptions.trustedDeviceOption) {
|
||||
accountDecryptionOptions.trustedDeviceOption = new TrustedDeviceUserDecryptionOption(
|
||||
responseOptions.trustedDeviceOption.hasAdminApproval,
|
||||
responseOptions.trustedDeviceOption.hasLoginApprovingDevice,
|
||||
responseOptions.trustedDeviceOption.hasManageResetPasswordPermission,
|
||||
);
|
||||
}
|
||||
|
||||
if (responseOptions.keyConnectorOption) {
|
||||
accountDecryptionOptions.keyConnectorOption = new KeyConnectorUserDecryptionOption(
|
||||
responseOptions.keyConnectorOption.keyConnectorUrl,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// If the response does not have userDecryptionOptions, this means it's on a pre-TDE server version and so
|
||||
// we must base our decryption options on the presence of the keyConnectorUrl.
|
||||
// Note that the presence of keyConnectorUrl implies that the user does not have a master password, as in pre-TDE
|
||||
// server versions, a master password short-circuited the addition of the keyConnectorUrl to the response.
|
||||
// TODO: remove this check after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3537)
|
||||
const usingKeyConnector = response.keyConnectorUrl != null;
|
||||
accountDecryptionOptions.hasMasterPassword = !usingKeyConnector;
|
||||
if (usingKeyConnector) {
|
||||
accountDecryptionOptions.keyConnectorOption = new KeyConnectorUserDecryptionOption(
|
||||
response.keyConnectorUrl,
|
||||
);
|
||||
}
|
||||
}
|
||||
return accountDecryptionOptions;
|
||||
}
|
||||
|
||||
static fromJSON(obj: Jsonify<AccountDecryptionOptions>): AccountDecryptionOptions {
|
||||
if (obj == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const accountDecryptionOptions = Object.assign(new AccountDecryptionOptions(), obj);
|
||||
|
||||
if (obj.trustedDeviceOption) {
|
||||
accountDecryptionOptions.trustedDeviceOption = new TrustedDeviceUserDecryptionOption(
|
||||
obj.trustedDeviceOption.hasAdminApproval,
|
||||
obj.trustedDeviceOption.hasLoginApprovingDevice,
|
||||
obj.trustedDeviceOption.hasManageResetPasswordPermission,
|
||||
);
|
||||
}
|
||||
|
||||
if (obj.keyConnectorOption) {
|
||||
accountDecryptionOptions.keyConnectorOption = new KeyConnectorUserDecryptionOption(
|
||||
obj.keyConnectorOption.keyConnectorUrl,
|
||||
);
|
||||
}
|
||||
|
||||
return accountDecryptionOptions;
|
||||
}
|
||||
}
|
||||
|
||||
export class Account {
|
||||
data?: AccountData = new AccountData();
|
||||
keys?: AccountKeys = new AccountKeys();
|
||||
profile?: AccountProfile = new AccountProfile();
|
||||
settings?: AccountSettings = new AccountSettings();
|
||||
tokens?: AccountTokens = new AccountTokens();
|
||||
decryptionOptions?: AccountDecryptionOptions = new AccountDecryptionOptions();
|
||||
adminAuthRequest?: Jsonify<AdminAuthRequestStorable> = null;
|
||||
|
||||
constructor(init: Partial<Account>) {
|
||||
|
@ -356,10 +262,6 @@ export class Account {
|
|||
...new AccountTokens(),
|
||||
...init?.tokens,
|
||||
},
|
||||
decryptionOptions: {
|
||||
...new AccountDecryptionOptions(),
|
||||
...init?.decryptionOptions,
|
||||
},
|
||||
adminAuthRequest: init?.adminAuthRequest,
|
||||
});
|
||||
}
|
||||
|
@ -375,7 +277,6 @@ export class Account {
|
|||
profile: AccountProfile.fromJSON(json?.profile),
|
||||
settings: AccountSettings.fromJSON(json?.settings),
|
||||
tokens: AccountTokens.fromJSON(json?.tokens),
|
||||
decryptionOptions: AccountDecryptionOptions.fromJSON(json?.decryptionOptions),
|
||||
adminAuthRequest: AdminAuthRequestStorable.fromJSON(json?.adminAuthRequest),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -34,12 +34,7 @@ import { HtmlStorageLocation, KdfType, StorageLocation } from "../enums";
|
|||
import { StateFactory } from "../factories/state-factory";
|
||||
import { Utils } from "../misc/utils";
|
||||
import { ServerConfigData } from "../models/data/server-config.data";
|
||||
import {
|
||||
Account,
|
||||
AccountData,
|
||||
AccountDecryptionOptions,
|
||||
AccountSettings,
|
||||
} from "../models/domain/account";
|
||||
import { Account, AccountData, AccountSettings } from "../models/domain/account";
|
||||
import { EncString } from "../models/domain/enc-string";
|
||||
import { GlobalState } from "../models/domain/global-state";
|
||||
import { State } from "../models/domain/state";
|
||||
|
@ -817,37 +812,6 @@ export class StateService<
|
|||
await this.saveAccount(account, options);
|
||||
}
|
||||
|
||||
async getAccountDecryptionOptions(
|
||||
options?: StorageOptions,
|
||||
): Promise<AccountDecryptionOptions | null> {
|
||||
options = this.reconcileOptions(options, await this.defaultOnDiskLocalOptions());
|
||||
|
||||
if (options?.userId == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const account = await this.getAccount(options);
|
||||
|
||||
return account?.decryptionOptions as AccountDecryptionOptions;
|
||||
}
|
||||
|
||||
async setAccountDecryptionOptions(
|
||||
value: AccountDecryptionOptions,
|
||||
options?: StorageOptions,
|
||||
): Promise<void> {
|
||||
options = this.reconcileOptions(options, await this.defaultOnDiskLocalOptions());
|
||||
|
||||
if (options?.userId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const account = await this.getAccount(options);
|
||||
|
||||
account.decryptionOptions = value;
|
||||
|
||||
await this.saveAccount(account, options);
|
||||
}
|
||||
|
||||
async getEmail(options?: StorageOptions): Promise<string> {
|
||||
return (
|
||||
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
|
||||
|
|
|
@ -44,6 +44,7 @@ export const TOKEN_DISK_LOCAL = new StateDefinition("tokenDiskLocal", "disk", {
|
|||
});
|
||||
export const TOKEN_MEMORY = new StateDefinition("token", "memory");
|
||||
export const LOGIN_STRATEGY_MEMORY = new StateDefinition("loginStrategy", "memory");
|
||||
export const USER_DECRYPTION_OPTIONS_DISK = new StateDefinition("userDecryptionOptions", "disk");
|
||||
|
||||
// Autofill
|
||||
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { firstValueFrom, of } from "rxjs";
|
||||
import { BehaviorSubject, firstValueFrom, map, of } from "rxjs";
|
||||
|
||||
import {
|
||||
FakeUserDecryptionOptions as UserDecryptionOptions,
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
|
||||
import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { Policy } from "../../admin-console/models/domain/policy";
|
||||
|
@ -8,12 +13,12 @@ import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum";
|
|||
import { CryptoService } from "../../platform/abstractions/crypto.service";
|
||||
import { StateService } from "../../platform/abstractions/state.service";
|
||||
import { BiometricStateService } from "../../platform/biometrics/biometric-state.service";
|
||||
import { AccountDecryptionOptions } from "../../platform/models/domain/account";
|
||||
import { EncString } from "../../platform/models/domain/enc-string";
|
||||
|
||||
import { VaultTimeoutSettingsService } from "./vault-timeout-settings.service";
|
||||
|
||||
describe("VaultTimeoutSettingsService", () => {
|
||||
let userDecryptionOptionsService: MockProxy<UserDecryptionOptionsServiceAbstraction>;
|
||||
let cryptoService: MockProxy<CryptoService>;
|
||||
let tokenService: MockProxy<TokenService>;
|
||||
let policyService: MockProxy<PolicyService>;
|
||||
|
@ -21,12 +26,26 @@ describe("VaultTimeoutSettingsService", () => {
|
|||
const biometricStateService = mock<BiometricStateService>();
|
||||
let service: VaultTimeoutSettingsService;
|
||||
|
||||
let userDecryptionOptionsSubject: BehaviorSubject<UserDecryptionOptions>;
|
||||
|
||||
beforeEach(() => {
|
||||
userDecryptionOptionsService = mock<UserDecryptionOptionsServiceAbstraction>();
|
||||
cryptoService = mock<CryptoService>();
|
||||
tokenService = mock<TokenService>();
|
||||
policyService = mock<PolicyService>();
|
||||
stateService = mock<StateService>();
|
||||
|
||||
userDecryptionOptionsSubject = new BehaviorSubject(null);
|
||||
userDecryptionOptionsService.userDecryptionOptions$ = userDecryptionOptionsSubject;
|
||||
userDecryptionOptionsService.hasMasterPassword$ = userDecryptionOptionsSubject.pipe(
|
||||
map((options) => options?.hasMasterPassword ?? false),
|
||||
);
|
||||
userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue(
|
||||
userDecryptionOptionsSubject,
|
||||
);
|
||||
|
||||
service = new VaultTimeoutSettingsService(
|
||||
userDecryptionOptionsService,
|
||||
cryptoService,
|
||||
tokenService,
|
||||
policyService,
|
||||
|
@ -49,9 +68,7 @@ describe("VaultTimeoutSettingsService", () => {
|
|||
});
|
||||
|
||||
it("contains Lock when the user has a master password", async () => {
|
||||
stateService.getAccountDecryptionOptions.mockResolvedValue(
|
||||
new AccountDecryptionOptions({ hasMasterPassword: true }),
|
||||
);
|
||||
userDecryptionOptionsSubject.next(new UserDecryptionOptions({ hasMasterPassword: true }));
|
||||
|
||||
const result = await firstValueFrom(service.availableVaultTimeoutActions$());
|
||||
|
||||
|
@ -83,9 +100,7 @@ describe("VaultTimeoutSettingsService", () => {
|
|||
});
|
||||
|
||||
it("not contains Lock when the user does not have a master password, PIN, or biometrics", async () => {
|
||||
stateService.getAccountDecryptionOptions.mockResolvedValue(
|
||||
new AccountDecryptionOptions({ hasMasterPassword: false }),
|
||||
);
|
||||
userDecryptionOptionsSubject.next(new UserDecryptionOptions({ hasMasterPassword: false }));
|
||||
stateService.getPinKeyEncryptedUserKey.mockResolvedValue(null);
|
||||
stateService.getProtectedPin.mockResolvedValue(null);
|
||||
biometricStateService.biometricUnlockEnabled$ = of(false);
|
||||
|
@ -107,9 +122,7 @@ describe("VaultTimeoutSettingsService", () => {
|
|||
`(
|
||||
"returns $expected when policy is $policy, and user preference is $userPreference",
|
||||
async ({ policy, userPreference, expected }) => {
|
||||
stateService.getAccountDecryptionOptions.mockResolvedValue(
|
||||
new AccountDecryptionOptions({ hasMasterPassword: true }),
|
||||
);
|
||||
userDecryptionOptionsSubject.next(new UserDecryptionOptions({ hasMasterPassword: true }));
|
||||
policyService.getAll$.mockReturnValue(
|
||||
of(policy === null ? [] : ([{ data: { action: policy } }] as unknown as Policy[])),
|
||||
);
|
||||
|
@ -136,8 +149,8 @@ describe("VaultTimeoutSettingsService", () => {
|
|||
"returns $expected when policy is $policy, has unlock method is $unlockMethod, and user preference is $userPreference",
|
||||
async ({ unlockMethod, policy, userPreference, expected }) => {
|
||||
biometricStateService.biometricUnlockEnabled$ = of(unlockMethod);
|
||||
stateService.getAccountDecryptionOptions.mockResolvedValue(
|
||||
new AccountDecryptionOptions({ hasMasterPassword: false }),
|
||||
userDecryptionOptionsSubject.next(
|
||||
new UserDecryptionOptions({ hasMasterPassword: false }),
|
||||
);
|
||||
policyService.getAll$.mockReturnValue(
|
||||
of(policy === null ? [] : ([{ data: { action: policy } }] as unknown as Policy[])),
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import { defer, firstValueFrom } from "rxjs";
|
||||
|
||||
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
||||
|
||||
import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "../../abstractions/vault-timeout/vault-timeout-settings.service";
|
||||
import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "../../admin-console/enums";
|
||||
|
@ -19,6 +21,7 @@ export type PinLockType = "DISABLED" | "PERSISTANT" | "TRANSIENT";
|
|||
|
||||
export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceAbstraction {
|
||||
constructor(
|
||||
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
||||
private cryptoService: CryptoService,
|
||||
private tokenService: TokenService,
|
||||
private policyService: PolicyService,
|
||||
|
@ -174,12 +177,15 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA
|
|||
}
|
||||
|
||||
private async userHasMasterPassword(userId: string): Promise<boolean> {
|
||||
const acctDecryptionOpts = await this.stateService.getAccountDecryptionOptions({
|
||||
userId: userId,
|
||||
});
|
||||
if (userId) {
|
||||
const decryptionOptions = await firstValueFrom(
|
||||
this.userDecryptionOptionsService.userDecryptionOptionsById$(userId),
|
||||
);
|
||||
|
||||
if (acctDecryptionOpts?.hasMasterPassword != undefined) {
|
||||
return acctDecryptionOpts.hasMasterPassword;
|
||||
if (decryptionOptions?.hasMasterPassword != undefined) {
|
||||
return decryptionOptions.hasMasterPassword;
|
||||
}
|
||||
}
|
||||
return await firstValueFrom(this.userDecryptionOptionsService.hasMasterPassword$);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,6 +39,7 @@ import { OrganizationMigrator } from "./migrations/40-move-organization-state-to
|
|||
import { EventCollectionMigrator } from "./migrations/41-move-event-collection-to-state-provider";
|
||||
import { EnableFaviconMigrator } from "./migrations/42-move-enable-favicon-to-domain-settings-state-provider";
|
||||
import { AutoConfirmFingerPrintsMigrator } from "./migrations/43-move-auto-confirm-finger-prints-to-state-provider";
|
||||
import { UserDecryptionOptionsMigrator } from "./migrations/44-move-user-decryption-options-to-state-provider";
|
||||
import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org-keys";
|
||||
import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key";
|
||||
import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account";
|
||||
|
@ -47,7 +48,7 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting
|
|||
import { MinVersionMigrator } from "./migrations/min-version";
|
||||
|
||||
export const MIN_VERSION = 3;
|
||||
export const CURRENT_VERSION = 43;
|
||||
export const CURRENT_VERSION = 44;
|
||||
export type MinVersion = typeof MIN_VERSION;
|
||||
|
||||
export function createMigrationBuilder() {
|
||||
|
@ -92,7 +93,8 @@ export function createMigrationBuilder() {
|
|||
.with(OrganizationMigrator, 39, 40)
|
||||
.with(EventCollectionMigrator, 40, 41)
|
||||
.with(EnableFaviconMigrator, 41, 42)
|
||||
.with(AutoConfirmFingerPrintsMigrator, 42, CURRENT_VERSION);
|
||||
.with(AutoConfirmFingerPrintsMigrator, 42, 43)
|
||||
.with(UserDecryptionOptionsMigrator, 43, CURRENT_VERSION);
|
||||
}
|
||||
|
||||
export async function currentVersion(
|
||||
|
|
|
@ -0,0 +1,238 @@
|
|||
import { any, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { MigrationHelper } from "../migration-helper";
|
||||
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||
|
||||
import { UserDecryptionOptionsMigrator } from "./44-move-user-decryption-options-to-state-provider";
|
||||
|
||||
function exampleJSON() {
|
||||
return {
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["FirstAccount", "SecondAccount", "ThirdAccount"],
|
||||
FirstAccount: {
|
||||
decryptionOptions: {
|
||||
hasMasterPassword: true,
|
||||
trustedDeviceOption: {
|
||||
hasAdminApproval: false,
|
||||
hasLoginApprovingDevice: false,
|
||||
hasManageResetPasswordPermission: true,
|
||||
},
|
||||
keyConnectorOption: {
|
||||
keyConnectorUrl: "https://keyconnector.bitwarden.com",
|
||||
},
|
||||
},
|
||||
profile: {
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
SecondAccount: {
|
||||
decryptionOptions: {
|
||||
hasMasterPassword: false,
|
||||
trustedDeviceOption: {
|
||||
hasAdminApproval: true,
|
||||
hasLoginApprovingDevice: true,
|
||||
hasManageResetPasswordPermission: true,
|
||||
},
|
||||
keyConnectorOption: {
|
||||
keyConnectorUrl: "https://selfhosted.bitwarden.com",
|
||||
},
|
||||
},
|
||||
profile: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function rollbackJSON() {
|
||||
return {
|
||||
user_FirstAccount_decryptionOptions_userDecryptionOptions: {
|
||||
hasMasterPassword: true,
|
||||
trustedDeviceOption: {
|
||||
hasAdminApproval: false,
|
||||
hasLoginApprovingDevice: false,
|
||||
hasManageResetPasswordPermission: true,
|
||||
},
|
||||
keyConnectorOption: {
|
||||
keyConnectorUrl: "https://keyconnector.bitwarden.com",
|
||||
},
|
||||
},
|
||||
user_SecondAccount_decryptionOptions_userDecryptionOptions: {
|
||||
hasMasterPassword: false,
|
||||
trustedDeviceOption: {
|
||||
hasAdminApproval: true,
|
||||
hasLoginApprovingDevice: true,
|
||||
hasManageResetPasswordPermission: true,
|
||||
},
|
||||
keyConnectorOption: {
|
||||
keyConnectorUrl: "https://selfhosted.bitwarden.com",
|
||||
},
|
||||
},
|
||||
user_ThirdAccount_decryptionOptions_userDecryptionOptions: {},
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["FirstAccount", "SecondAccount", "ThirdAccount"],
|
||||
FirstAccount: {
|
||||
decryptionOptions: {
|
||||
hasMasterPassword: true,
|
||||
trustedDeviceOption: {
|
||||
hasAdminApproval: false,
|
||||
hasLoginApprovingDevice: false,
|
||||
hasManageResetPasswordPermission: true,
|
||||
},
|
||||
keyConnectorOption: {
|
||||
keyConnectorUrl: "https://keyconnector.bitwarden.com",
|
||||
},
|
||||
},
|
||||
profile: {
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
SecondAccount: {
|
||||
decryptionOptions: {
|
||||
hasMasterPassword: false,
|
||||
trustedDeviceOption: {
|
||||
hasAdminApproval: true,
|
||||
hasLoginApprovingDevice: true,
|
||||
hasManageResetPasswordPermission: true,
|
||||
},
|
||||
keyConnectorOption: {
|
||||
keyConnectorUrl: "https://selfhosted.bitwarden.com",
|
||||
},
|
||||
},
|
||||
profile: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("UserDecryptionOptionsMigrator", () => {
|
||||
let helper: MockProxy<MigrationHelper>;
|
||||
let sut: UserDecryptionOptionsMigrator;
|
||||
const keyDefinitionLike = {
|
||||
key: "decryptionOptions",
|
||||
stateDefinition: {
|
||||
name: "userDecryptionOptions",
|
||||
},
|
||||
};
|
||||
|
||||
describe("migrate", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(exampleJSON(), 43);
|
||||
sut = new UserDecryptionOptionsMigrator(43, 44);
|
||||
});
|
||||
|
||||
it("should remove decryptionOptions from all accounts", async () => {
|
||||
await sut.migrate(helper);
|
||||
expect(helper.set).toHaveBeenCalledWith("FirstAccount", {
|
||||
profile: {
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
expect(helper.set).toHaveBeenCalledWith("SecondAccount", {
|
||||
profile: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
});
|
||||
});
|
||||
|
||||
it("should set decryptionOptions provider value for each account", async () => {
|
||||
await sut.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("FirstAccount", keyDefinitionLike, {
|
||||
hasMasterPassword: true,
|
||||
trustedDeviceOption: {
|
||||
hasAdminApproval: false,
|
||||
hasLoginApprovingDevice: false,
|
||||
hasManageResetPasswordPermission: true,
|
||||
},
|
||||
keyConnectorOption: {
|
||||
keyConnectorUrl: "https://keyconnector.bitwarden.com",
|
||||
},
|
||||
});
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("SecondAccount", keyDefinitionLike, {
|
||||
hasMasterPassword: false,
|
||||
trustedDeviceOption: {
|
||||
hasAdminApproval: true,
|
||||
hasLoginApprovingDevice: true,
|
||||
hasManageResetPasswordPermission: true,
|
||||
},
|
||||
keyConnectorOption: {
|
||||
keyConnectorUrl: "https://selfhosted.bitwarden.com",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("rollback", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(rollbackJSON(), 44);
|
||||
sut = new UserDecryptionOptionsMigrator(43, 44);
|
||||
});
|
||||
|
||||
it.each(["FirstAccount", "SecondAccount", "ThirdAccount"])(
|
||||
"should null out new values",
|
||||
async (userId) => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(userId, keyDefinitionLike, null);
|
||||
},
|
||||
);
|
||||
|
||||
it("should add explicit value back to accounts", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).toHaveBeenCalledWith("FirstAccount", {
|
||||
decryptionOptions: {
|
||||
hasMasterPassword: true,
|
||||
trustedDeviceOption: {
|
||||
hasAdminApproval: false,
|
||||
hasLoginApprovingDevice: false,
|
||||
hasManageResetPasswordPermission: true,
|
||||
},
|
||||
keyConnectorOption: {
|
||||
keyConnectorUrl: "https://keyconnector.bitwarden.com",
|
||||
},
|
||||
},
|
||||
profile: {
|
||||
otherStuff: "overStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
expect(helper.set).toHaveBeenCalledWith("SecondAccount", {
|
||||
decryptionOptions: {
|
||||
hasMasterPassword: false,
|
||||
trustedDeviceOption: {
|
||||
hasAdminApproval: true,
|
||||
hasLoginApprovingDevice: true,
|
||||
hasManageResetPasswordPermission: true,
|
||||
},
|
||||
keyConnectorOption: {
|
||||
keyConnectorUrl: "https://selfhosted.bitwarden.com",
|
||||
},
|
||||
},
|
||||
profile: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
});
|
||||
});
|
||||
|
||||
it("should not try to restore values to missing accounts", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).not.toHaveBeenCalledWith("ThirdAccount", any());
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,57 @@
|
|||
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
type DecryptionOptionsType = {
|
||||
hasMasterPassword: boolean;
|
||||
trustedDeviceOption?: {
|
||||
hasAdminApproval: boolean;
|
||||
hasLoginApprovingDevice: boolean;
|
||||
hasManageResetPasswordPermission: boolean;
|
||||
};
|
||||
keyConnectorOption?: {
|
||||
keyConnectorUrl: string;
|
||||
};
|
||||
};
|
||||
|
||||
type ExpectedAccountType = {
|
||||
decryptionOptions?: DecryptionOptionsType;
|
||||
};
|
||||
|
||||
const USER_DECRYPTION_OPTIONS: KeyDefinitionLike = {
|
||||
key: "decryptionOptions",
|
||||
stateDefinition: {
|
||||
name: "userDecryptionOptions",
|
||||
},
|
||||
};
|
||||
|
||||
export class UserDecryptionOptionsMigrator extends Migrator<43, 44> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
async function migrateAccount(userId: string, account: ExpectedAccountType): Promise<void> {
|
||||
const value = account?.decryptionOptions;
|
||||
if (value != null) {
|
||||
await helper.setToUser(userId, USER_DECRYPTION_OPTIONS, value);
|
||||
delete account.decryptionOptions;
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]);
|
||||
}
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise<void> {
|
||||
const value: DecryptionOptionsType = await helper.getFromUser(
|
||||
userId,
|
||||
USER_DECRYPTION_OPTIONS,
|
||||
);
|
||||
if (account) {
|
||||
account.decryptionOptions = Object.assign(account.decryptionOptions, value);
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
await helper.setToUser(userId, USER_DECRYPTION_OPTIONS, null);
|
||||
}
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]);
|
||||
}
|
||||
}
|
|
@ -1,3 +1,7 @@
|
|||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
||||
|
||||
import { ApiService } from "../../../abstractions/api.service";
|
||||
import { InternalOrganizationServiceAbstraction } from "../../../admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { InternalPolicyService } from "../../../admin-console/abstractions/policy/policy.service.abstraction";
|
||||
|
@ -24,7 +28,6 @@ import { LogService } from "../../../platform/abstractions/log.service";
|
|||
import { MessagingService } from "../../../platform/abstractions/messaging.service";
|
||||
import { StateService } from "../../../platform/abstractions/state.service";
|
||||
import { sequentialize } from "../../../platform/misc/sequentialize";
|
||||
import { AccountDecryptionOptions } from "../../../platform/models/domain/account";
|
||||
import { SendData } from "../../../tools/send/models/data/send.data";
|
||||
import { SendResponse } from "../../../tools/send/models/response/send.response";
|
||||
import { SendApiService } from "../../../tools/send/services/send-api.service.abstraction";
|
||||
|
@ -62,6 +65,7 @@ export class SyncService implements SyncServiceAbstraction {
|
|||
private folderApiService: FolderApiServiceAbstraction,
|
||||
private organizationService: InternalOrganizationServiceAbstraction,
|
||||
private sendApiService: SendApiService,
|
||||
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
||||
private avatarService: AvatarService,
|
||||
private logoutCallback: (expired: boolean) => Promise<void>,
|
||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
|
@ -353,19 +357,12 @@ export class SyncService implements SyncServiceAbstraction {
|
|||
);
|
||||
}
|
||||
|
||||
const acctDecryptionOpts: AccountDecryptionOptions =
|
||||
await this.stateService.getAccountDecryptionOptions();
|
||||
const userDecryptionOptions = await firstValueFrom(
|
||||
this.userDecryptionOptionsService.userDecryptionOptions$,
|
||||
);
|
||||
|
||||
// Account decryption options should never be null or undefined b/c it is always initialized
|
||||
// during the processing of the ID token response, but there might be a state issue
|
||||
// where it is being overwritten with undefined affecting browser extension + FireFox users.
|
||||
// TODO: Consider removing this once we figure out the root cause of the state issue or after the state provider refactor.
|
||||
if (acctDecryptionOpts === null || acctDecryptionOpts === undefined) {
|
||||
if (userDecryptionOptions === null || userDecryptionOptions === undefined) {
|
||||
this.logService.error("Sync: Account decryption options are null or undefined.");
|
||||
// Early return as a bandaid to allow the rest of the sync to continue so users can access
|
||||
// their data that they might have added from another device.
|
||||
// Otherwise, trying to access properties on undefined below will throw an error.
|
||||
return;
|
||||
}
|
||||
|
||||
// Even though TDE users should only be in a single org (per single org policy), check
|
||||
|
@ -384,8 +381,8 @@ export class SyncService implements SyncServiceAbstraction {
|
|||
}
|
||||
|
||||
if (
|
||||
acctDecryptionOpts.trustedDeviceOption !== undefined &&
|
||||
!acctDecryptionOpts.hasMasterPassword &&
|
||||
userDecryptionOptions.trustedDeviceOption !== undefined &&
|
||||
!userDecryptionOptions.hasMasterPassword &&
|
||||
hasManageResetPasswordPermission
|
||||
) {
|
||||
// TDE user w/out MP went from having no password reset permission to having it.
|
||||
|
|
Loading…
Reference in New Issue