mirror of
https://github.com/bitwarden/browser
synced 2026-02-06 11:43:51 +00:00
Merge branch 'auth/pm-19877/notification-processing', remote-tracking branch 'origin' into auth/pm-23620/auth-request-answering-service
This commit is contained in:
@@ -1,232 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive, OnDestroy, OnInit } from "@angular/core";
|
||||
import { Subject, firstValueFrom, map, switchMap, takeUntil } from "rxjs";
|
||||
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { UserKey, MasterKey } from "@bitwarden/common/types/key";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { KdfConfig, KdfConfigService, KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { PasswordColorText } from "../../tools/password-strength/password-strength.component";
|
||||
|
||||
@Directive()
|
||||
export class ChangePasswordComponent implements OnInit, OnDestroy {
|
||||
masterPassword: string;
|
||||
masterPasswordRetype: string;
|
||||
formPromise: Promise<any>;
|
||||
enforcedPolicyOptions: MasterPasswordPolicyOptions;
|
||||
passwordStrengthResult: any;
|
||||
color: string;
|
||||
text: string;
|
||||
leakedPassword: boolean;
|
||||
minimumLength = Utils.minimumPasswordLength;
|
||||
|
||||
protected email: string;
|
||||
protected kdfConfig: KdfConfig;
|
||||
|
||||
protected destroy$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
protected accountService: AccountService,
|
||||
protected dialogService: DialogService,
|
||||
protected i18nService: I18nService,
|
||||
protected kdfConfigService: KdfConfigService,
|
||||
protected keyService: KeyService,
|
||||
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
protected messagingService: MessagingService,
|
||||
protected platformUtilsService: PlatformUtilsService,
|
||||
protected policyService: PolicyService,
|
||||
protected toastService: ToastService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.email = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.email)),
|
||||
);
|
||||
this.accountService.activeAccount$
|
||||
.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.policyService.masterPasswordPolicyOptions$(userId)),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe(
|
||||
(enforcedPasswordPolicyOptions) =>
|
||||
(this.enforcedPolicyOptions ??= enforcedPasswordPolicyOptions),
|
||||
);
|
||||
|
||||
if (this.enforcedPolicyOptions?.minLength) {
|
||||
this.minimumLength = this.enforcedPolicyOptions.minLength;
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
async submit() {
|
||||
if (!(await this.strongPassword())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(await this.setupSubmitActions())) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [userId, email] = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => [a?.id, a?.email])),
|
||||
);
|
||||
|
||||
if (this.kdfConfig == null) {
|
||||
this.kdfConfig = await this.kdfConfigService.getKdfConfig(userId);
|
||||
}
|
||||
|
||||
// Create new master key
|
||||
const newMasterKey = await this.keyService.makeMasterKey(
|
||||
this.masterPassword,
|
||||
email.trim().toLowerCase(),
|
||||
this.kdfConfig,
|
||||
);
|
||||
const newMasterKeyHash = await this.keyService.hashMasterKey(this.masterPassword, newMasterKey);
|
||||
|
||||
let newProtectedUserKey: [UserKey, EncString] = null;
|
||||
const userKey = await this.keyService.getUserKey();
|
||||
if (userKey == null) {
|
||||
newProtectedUserKey = await this.keyService.makeUserKey(newMasterKey);
|
||||
} else {
|
||||
newProtectedUserKey = await this.keyService.encryptUserKeyWithMasterKey(newMasterKey);
|
||||
}
|
||||
|
||||
await this.performSubmitActions(newMasterKeyHash, newMasterKey, newProtectedUserKey);
|
||||
}
|
||||
|
||||
async setupSubmitActions(): Promise<boolean> {
|
||||
// Override in sub-class
|
||||
// Can be used for additional validation and/or other processes the should occur before changing passwords
|
||||
return true;
|
||||
}
|
||||
|
||||
async performSubmitActions(
|
||||
newMasterKeyHash: string,
|
||||
newMasterKey: MasterKey,
|
||||
newUserKey: [UserKey, EncString],
|
||||
) {
|
||||
// Override in sub-class
|
||||
}
|
||||
|
||||
async strongPassword(): Promise<boolean> {
|
||||
if (this.masterPassword == null || this.masterPassword === "") {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("masterPasswordRequired"),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
if (this.masterPassword.length < this.minimumLength) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("masterPasswordMinimumlength", this.minimumLength),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
if (this.masterPassword !== this.masterPasswordRetype) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("masterPassDoesntMatch"),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
const strengthResult = this.passwordStrengthResult;
|
||||
|
||||
if (
|
||||
this.enforcedPolicyOptions != null &&
|
||||
!this.policyService.evaluateMasterPassword(
|
||||
strengthResult.score,
|
||||
this.masterPassword,
|
||||
this.enforcedPolicyOptions,
|
||||
)
|
||||
) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("masterPasswordPolicyRequirementsNotMet"),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
const weakPassword = strengthResult != null && strengthResult.score < 3;
|
||||
|
||||
if (weakPassword && this.leakedPassword) {
|
||||
const result = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "weakAndExposedMasterPassword" },
|
||||
content: { key: "weakAndBreachedMasterPasswordDesc" },
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (weakPassword) {
|
||||
const result = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "weakMasterPassword" },
|
||||
content: { key: "weakMasterPasswordDesc" },
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (this.leakedPassword) {
|
||||
const result = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "exposedMasterPassword" },
|
||||
content: { key: "exposedMasterPasswordDesc" },
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async logOut() {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "logOut" },
|
||||
content: { key: "logOutConfirmation" },
|
||||
acceptButtonText: { key: "logOut" },
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (confirmed) {
|
||||
this.messagingService.send("logout");
|
||||
}
|
||||
}
|
||||
|
||||
getStrengthResult(result: any) {
|
||||
this.passwordStrengthResult = result;
|
||||
}
|
||||
|
||||
getPasswordScoreText(event: PasswordColorText) {
|
||||
this.color = event.color;
|
||||
this.text = event.text;
|
||||
}
|
||||
}
|
||||
@@ -1,300 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { firstValueFrom, of } from "rxjs";
|
||||
import { filter, first, switchMap, tap } from "rxjs/operators";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import {
|
||||
OrganizationUserApiService,
|
||||
OrganizationUserResetPasswordEnrollmentRequest,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { OrganizationAutoEnrollStatusResponse } from "@bitwarden/common/admin-console/models/response/organization-auto-enroll-status.response";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { HashPurpose } from "@bitwarden/common/platform/enums";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { DEFAULT_KDF_CONFIG, KdfConfigService, KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { ChangePasswordComponent as BaseChangePasswordComponent } from "./change-password.component";
|
||||
|
||||
@Directive()
|
||||
export class SetPasswordComponent extends BaseChangePasswordComponent implements OnInit {
|
||||
syncLoading = true;
|
||||
showPassword = false;
|
||||
hint = "";
|
||||
orgSsoIdentifier: string = null;
|
||||
orgId: string;
|
||||
resetPasswordAutoEnroll = false;
|
||||
onSuccessfulChangePassword: () => Promise<void>;
|
||||
successRoute = "vault";
|
||||
activeUserId: UserId;
|
||||
|
||||
forceSetPasswordReason: ForceSetPasswordReason = ForceSetPasswordReason.None;
|
||||
ForceSetPasswordReason = ForceSetPasswordReason;
|
||||
|
||||
constructor(
|
||||
protected accountService: AccountService,
|
||||
protected dialogService: DialogService,
|
||||
protected encryptService: EncryptService,
|
||||
protected i18nService: I18nService,
|
||||
protected kdfConfigService: KdfConfigService,
|
||||
protected keyService: KeyService,
|
||||
protected masterPasswordApiService: MasterPasswordApiService,
|
||||
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
protected messagingService: MessagingService,
|
||||
protected organizationApiService: OrganizationApiServiceAbstraction,
|
||||
protected organizationUserApiService: OrganizationUserApiService,
|
||||
protected platformUtilsService: PlatformUtilsService,
|
||||
protected policyApiService: PolicyApiServiceAbstraction,
|
||||
protected policyService: PolicyService,
|
||||
protected route: ActivatedRoute,
|
||||
protected router: Router,
|
||||
protected ssoLoginService: SsoLoginServiceAbstraction,
|
||||
protected syncService: SyncService,
|
||||
protected toastService: ToastService,
|
||||
protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
|
||||
) {
|
||||
super(
|
||||
accountService,
|
||||
dialogService,
|
||||
i18nService,
|
||||
kdfConfigService,
|
||||
keyService,
|
||||
masterPasswordService,
|
||||
messagingService,
|
||||
platformUtilsService,
|
||||
policyService,
|
||||
toastService,
|
||||
);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
super.ngOnInit();
|
||||
|
||||
await this.syncService.fullSync(true);
|
||||
this.syncLoading = false;
|
||||
|
||||
this.activeUserId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
|
||||
this.forceSetPasswordReason = await firstValueFrom(
|
||||
this.masterPasswordService.forceSetPasswordReason$(this.activeUserId),
|
||||
);
|
||||
|
||||
this.route.queryParams
|
||||
.pipe(
|
||||
first(),
|
||||
switchMap((qParams) => {
|
||||
if (qParams.identifier != null) {
|
||||
return of(qParams.identifier);
|
||||
} else {
|
||||
// Try to get orgSsoId from state as fallback
|
||||
// Note: this is primarily for the TDE user w/out MP obtains admin MP reset permission scenario.
|
||||
return this.ssoLoginService.getActiveUserOrganizationSsoIdentifier(this.activeUserId);
|
||||
}
|
||||
}),
|
||||
filter((orgSsoId) => orgSsoId != null),
|
||||
tap((orgSsoId: string) => {
|
||||
this.orgSsoIdentifier = orgSsoId;
|
||||
}),
|
||||
switchMap((orgSsoId: string) => this.organizationApiService.getAutoEnrollStatus(orgSsoId)),
|
||||
tap((orgAutoEnrollStatusResponse: OrganizationAutoEnrollStatusResponse) => {
|
||||
this.orgId = orgAutoEnrollStatusResponse.id;
|
||||
this.resetPasswordAutoEnroll = orgAutoEnrollStatusResponse.resetPasswordEnabled;
|
||||
}),
|
||||
switchMap((orgAutoEnrollStatusResponse: OrganizationAutoEnrollStatusResponse) =>
|
||||
// Must get org id from response to get master password policy options
|
||||
this.policyApiService.getMasterPasswordPolicyOptsForOrgUser(
|
||||
orgAutoEnrollStatusResponse.id,
|
||||
),
|
||||
),
|
||||
tap((masterPasswordPolicyOptions: MasterPasswordPolicyOptions) => {
|
||||
this.enforcedPolicyOptions = masterPasswordPolicyOptions;
|
||||
}),
|
||||
)
|
||||
.subscribe({
|
||||
error: () => {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: null,
|
||||
message: this.i18nService.t("errorOccurred"),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async setupSubmitActions() {
|
||||
this.kdfConfig = DEFAULT_KDF_CONFIG;
|
||||
return true;
|
||||
}
|
||||
|
||||
async performSubmitActions(
|
||||
masterPasswordHash: string,
|
||||
masterKey: MasterKey,
|
||||
userKey: [UserKey, EncString],
|
||||
) {
|
||||
let keysRequest: KeysRequest | null = null;
|
||||
let newKeyPair: [string, EncString] | null = null;
|
||||
|
||||
if (
|
||||
this.forceSetPasswordReason !=
|
||||
ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission
|
||||
) {
|
||||
// Existing JIT provisioned user in a MP encryption org setting first password
|
||||
// Users in this state will not already have a user asymmetric key pair so must create it for them
|
||||
// We don't want to re-create the user key pair if the user already has one (TDE user case)
|
||||
|
||||
// in case we have a local private key, and are not sure whether it has been posted to the server, we post the local private key instead of generating a new one
|
||||
const existingUserPrivateKey = (await firstValueFrom(
|
||||
this.keyService.userPrivateKey$(this.activeUserId),
|
||||
)) as Uint8Array;
|
||||
const existingUserPublicKey = await firstValueFrom(
|
||||
this.keyService.userPublicKey$(this.activeUserId),
|
||||
);
|
||||
if (existingUserPrivateKey != null && existingUserPublicKey != null) {
|
||||
const existingUserPublicKeyB64 = Utils.fromBufferToB64(existingUserPublicKey);
|
||||
newKeyPair = [
|
||||
existingUserPublicKeyB64,
|
||||
await this.encryptService.wrapDecapsulationKey(existingUserPrivateKey, userKey[0]),
|
||||
];
|
||||
} else {
|
||||
newKeyPair = await this.keyService.makeKeyPair(userKey[0]);
|
||||
}
|
||||
keysRequest = new KeysRequest(newKeyPair[0], newKeyPair[1].encryptedString);
|
||||
}
|
||||
|
||||
const request = new SetPasswordRequest(
|
||||
masterPasswordHash,
|
||||
userKey[1].encryptedString,
|
||||
this.hint,
|
||||
this.orgSsoIdentifier,
|
||||
keysRequest,
|
||||
this.kdfConfig.kdfType, //always PBKDF2 --> see this.setupSubmitActions
|
||||
this.kdfConfig.iterations,
|
||||
);
|
||||
try {
|
||||
if (this.resetPasswordAutoEnroll) {
|
||||
this.formPromise = this.masterPasswordApiService
|
||||
.setPassword(request)
|
||||
.then(async () => {
|
||||
await this.onSetPasswordSuccess(masterKey, userKey, newKeyPair);
|
||||
return this.organizationApiService.getKeys(this.orgId);
|
||||
})
|
||||
.then(async (response) => {
|
||||
if (response == null) {
|
||||
throw new Error(this.i18nService.t("resetPasswordOrgKeysError"));
|
||||
}
|
||||
const publicKey = Utils.fromB64ToArray(response.publicKey);
|
||||
|
||||
// RSA Encrypt user key with organization public key
|
||||
const userKey = await this.keyService.getUserKey();
|
||||
const encryptedUserKey = await this.encryptService.encapsulateKeyUnsigned(
|
||||
userKey,
|
||||
publicKey,
|
||||
);
|
||||
|
||||
const resetRequest = new OrganizationUserResetPasswordEnrollmentRequest();
|
||||
resetRequest.masterPasswordHash = masterPasswordHash;
|
||||
resetRequest.resetPasswordKey = encryptedUserKey.encryptedString;
|
||||
|
||||
return this.organizationUserApiService.putOrganizationUserResetPasswordEnrollment(
|
||||
this.orgId,
|
||||
this.activeUserId,
|
||||
resetRequest,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
this.formPromise = this.masterPasswordApiService.setPassword(request).then(async () => {
|
||||
await this.onSetPasswordSuccess(masterKey, userKey, newKeyPair);
|
||||
});
|
||||
}
|
||||
|
||||
await this.formPromise;
|
||||
|
||||
if (this.onSuccessfulChangePassword != null) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.onSuccessfulChangePassword();
|
||||
} else {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate([this.successRoute]);
|
||||
}
|
||||
} catch {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: null,
|
||||
message: this.i18nService.t("errorOccurred"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
togglePassword(confirmField: boolean) {
|
||||
this.showPassword = !this.showPassword;
|
||||
document.getElementById(confirmField ? "masterPasswordRetype" : "masterPassword").focus();
|
||||
}
|
||||
|
||||
protected async onSetPasswordSuccess(
|
||||
masterKey: MasterKey,
|
||||
userKey: [UserKey, EncString],
|
||||
keyPair: [string, EncString] | null,
|
||||
) {
|
||||
// Clear force set password reason to allow navigation back to vault.
|
||||
await this.masterPasswordService.setForceSetPasswordReason(
|
||||
ForceSetPasswordReason.None,
|
||||
this.activeUserId,
|
||||
);
|
||||
|
||||
// User now has a password so update account decryption options in state
|
||||
const userDecryptionOpts = await firstValueFrom(
|
||||
this.userDecryptionOptionsService.userDecryptionOptions$,
|
||||
);
|
||||
userDecryptionOpts.hasMasterPassword = true;
|
||||
await this.userDecryptionOptionsService.setUserDecryptionOptions(userDecryptionOpts);
|
||||
await this.kdfConfigService.setKdfConfig(this.activeUserId, this.kdfConfig);
|
||||
await this.masterPasswordService.setMasterKey(masterKey, this.activeUserId);
|
||||
await this.keyService.setUserKey(userKey[0], this.activeUserId);
|
||||
|
||||
// Set private key only for new JIT provisioned users in MP encryption orgs
|
||||
// Existing TDE users will have private key set on sync or on login
|
||||
if (
|
||||
keyPair !== null &&
|
||||
this.forceSetPasswordReason !=
|
||||
ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission
|
||||
) {
|
||||
await this.keyService.setPrivateKey(keyPair[1].encryptedString, this.activeUserId);
|
||||
}
|
||||
|
||||
const localMasterKeyHash = await this.keyService.hashMasterKey(
|
||||
this.masterPassword,
|
||||
masterKey,
|
||||
HashPurpose.LocalAuthorization,
|
||||
);
|
||||
await this.masterPasswordService.setMasterKeyHash(localMasterKeyHash, this.activeUserId);
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,9 @@ import { Directive, OnInit } from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { DialogRef } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
|
||||
import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { Verification } from "@bitwarden/common/auth/types/verification";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { KdfConfigService, KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { ChangePasswordComponent as BaseChangePasswordComponent } from "./change-password.component";
|
||||
|
||||
@Directive()
|
||||
export class UpdatePasswordComponent extends BaseChangePasswordComponent {
|
||||
hint: string;
|
||||
key: string;
|
||||
enforcedPolicyOptions: MasterPasswordPolicyOptions;
|
||||
showPassword = false;
|
||||
currentMasterPassword: string;
|
||||
|
||||
onSuccessfulChangePassword: () => Promise<void>;
|
||||
|
||||
constructor(
|
||||
protected router: Router,
|
||||
i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
policyService: PolicyService,
|
||||
keyService: KeyService,
|
||||
messagingService: MessagingService,
|
||||
private masterPasswordApiService: MasterPasswordApiService,
|
||||
private userVerificationService: UserVerificationService,
|
||||
private logService: LogService,
|
||||
dialogService: DialogService,
|
||||
kdfConfigService: KdfConfigService,
|
||||
masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
accountService: AccountService,
|
||||
toastService: ToastService,
|
||||
) {
|
||||
super(
|
||||
accountService,
|
||||
dialogService,
|
||||
i18nService,
|
||||
kdfConfigService,
|
||||
keyService,
|
||||
masterPasswordService,
|
||||
messagingService,
|
||||
platformUtilsService,
|
||||
policyService,
|
||||
toastService,
|
||||
);
|
||||
}
|
||||
|
||||
togglePassword(confirmField: boolean) {
|
||||
this.showPassword = !this.showPassword;
|
||||
document.getElementById(confirmField ? "masterPasswordRetype" : "masterPassword").focus();
|
||||
}
|
||||
|
||||
async cancel() {
|
||||
await this.router.navigate(["/vault"]);
|
||||
}
|
||||
|
||||
async setupSubmitActions(): Promise<boolean> {
|
||||
if (this.currentMasterPassword == null || this.currentMasterPassword === "") {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("masterPasswordRequired"),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
const secret: Verification = {
|
||||
type: VerificationType.MasterPassword,
|
||||
secret: this.currentMasterPassword,
|
||||
};
|
||||
try {
|
||||
await this.userVerificationService.verifyUser(secret);
|
||||
} catch (e) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: e.message,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
this.kdfConfig = await this.kdfConfigService.getKdfConfig(userId);
|
||||
return true;
|
||||
}
|
||||
|
||||
async performSubmitActions(
|
||||
newMasterKeyHash: string,
|
||||
newMasterKey: MasterKey,
|
||||
newUserKey: [UserKey, EncString],
|
||||
) {
|
||||
try {
|
||||
// Create Request
|
||||
const request = new PasswordRequest();
|
||||
request.masterPasswordHash = await this.keyService.hashMasterKey(
|
||||
this.currentMasterPassword,
|
||||
await this.keyService.getOrDeriveMasterKey(this.currentMasterPassword),
|
||||
);
|
||||
request.newMasterPasswordHash = newMasterKeyHash;
|
||||
request.key = newUserKey[1].encryptedString;
|
||||
|
||||
// Update user's password
|
||||
await this.masterPasswordApiService.postPassword(request);
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: this.i18nService.t("masterPasswordChanged"),
|
||||
message: this.i18nService.t("logBackIn"),
|
||||
});
|
||||
|
||||
if (this.onSuccessfulChangePassword != null) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.onSuccessfulChangePassword();
|
||||
} else {
|
||||
this.messagingService.send("logout");
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,232 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive, OnInit } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";
|
||||
import { UpdateTdeOffboardingPasswordRequest } from "@bitwarden/common/auth/models/request/update-tde-offboarding-password.request";
|
||||
import { UpdateTempPasswordRequest } from "@bitwarden/common/auth/models/request/update-temp-password.request";
|
||||
import { MasterPasswordVerification } from "@bitwarden/common/auth/types/verification";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { KdfConfigService, KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { ChangePasswordComponent as BaseChangePasswordComponent } from "./change-password.component";
|
||||
|
||||
@Directive()
|
||||
export class UpdateTempPasswordComponent extends BaseChangePasswordComponent implements OnInit {
|
||||
hint: string;
|
||||
key: string;
|
||||
enforcedPolicyOptions: MasterPasswordPolicyOptions;
|
||||
showPassword = false;
|
||||
reason: ForceSetPasswordReason = ForceSetPasswordReason.None;
|
||||
verification: MasterPasswordVerification = {
|
||||
type: VerificationType.MasterPassword,
|
||||
secret: "",
|
||||
};
|
||||
|
||||
onSuccessfulChangePassword: () => Promise<any>;
|
||||
|
||||
get requireCurrentPassword(): boolean {
|
||||
return this.reason === ForceSetPasswordReason.WeakMasterPassword;
|
||||
}
|
||||
|
||||
constructor(
|
||||
i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
policyService: PolicyService,
|
||||
keyService: KeyService,
|
||||
messagingService: MessagingService,
|
||||
private masterPasswordApiService: MasterPasswordApiService,
|
||||
private syncService: SyncService,
|
||||
private logService: LogService,
|
||||
private userVerificationService: UserVerificationService,
|
||||
protected router: Router,
|
||||
dialogService: DialogService,
|
||||
kdfConfigService: KdfConfigService,
|
||||
accountService: AccountService,
|
||||
masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
toastService: ToastService,
|
||||
) {
|
||||
super(
|
||||
accountService,
|
||||
dialogService,
|
||||
i18nService,
|
||||
kdfConfigService,
|
||||
keyService,
|
||||
masterPasswordService,
|
||||
messagingService,
|
||||
platformUtilsService,
|
||||
policyService,
|
||||
toastService,
|
||||
);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
await this.syncService.fullSync(true);
|
||||
|
||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
this.reason = await firstValueFrom(this.masterPasswordService.forceSetPasswordReason$(userId));
|
||||
|
||||
// If we somehow end up here without a reason, go back to the home page
|
||||
if (this.reason == ForceSetPasswordReason.None) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["/"]);
|
||||
return;
|
||||
}
|
||||
|
||||
await super.ngOnInit();
|
||||
}
|
||||
|
||||
get masterPasswordWarningText(): string {
|
||||
if (this.reason == ForceSetPasswordReason.WeakMasterPassword) {
|
||||
return this.i18nService.t("updateWeakMasterPasswordWarning");
|
||||
} else if (this.reason == ForceSetPasswordReason.TdeOffboarding) {
|
||||
return this.i18nService.t("tdeDisabledMasterPasswordRequired");
|
||||
} else {
|
||||
return this.i18nService.t("updateMasterPasswordWarning");
|
||||
}
|
||||
}
|
||||
|
||||
togglePassword(confirmField: boolean) {
|
||||
this.showPassword = !this.showPassword;
|
||||
document.getElementById(confirmField ? "masterPasswordRetype" : "masterPassword").focus();
|
||||
}
|
||||
|
||||
async setupSubmitActions(): Promise<boolean> {
|
||||
const [userId, email] = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => [a?.id, a?.email])),
|
||||
);
|
||||
this.email = email;
|
||||
this.kdfConfig = await this.kdfConfigService.getKdfConfig(userId);
|
||||
return true;
|
||||
}
|
||||
|
||||
async submit() {
|
||||
// Validation
|
||||
if (!(await this.strongPassword())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(await this.setupSubmitActions())) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Create new key and hash new password
|
||||
const newMasterKey = await this.keyService.makeMasterKey(
|
||||
this.masterPassword,
|
||||
this.email.trim().toLowerCase(),
|
||||
this.kdfConfig,
|
||||
);
|
||||
const newPasswordHash = await this.keyService.hashMasterKey(
|
||||
this.masterPassword,
|
||||
newMasterKey,
|
||||
);
|
||||
|
||||
// Grab user key
|
||||
const userKey = await this.keyService.getUserKey();
|
||||
|
||||
// Encrypt user key with new master key
|
||||
const newProtectedUserKey = await this.keyService.encryptUserKeyWithMasterKey(
|
||||
newMasterKey,
|
||||
userKey,
|
||||
);
|
||||
|
||||
await this.performSubmitActions(newPasswordHash, newMasterKey, newProtectedUserKey);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async performSubmitActions(
|
||||
masterPasswordHash: string,
|
||||
masterKey: MasterKey,
|
||||
userKey: [UserKey, EncString],
|
||||
) {
|
||||
try {
|
||||
switch (this.reason) {
|
||||
case ForceSetPasswordReason.AdminForcePasswordReset:
|
||||
this.formPromise = this.updateTempPassword(masterPasswordHash, userKey);
|
||||
break;
|
||||
case ForceSetPasswordReason.WeakMasterPassword:
|
||||
this.formPromise = this.updatePassword(masterPasswordHash, userKey);
|
||||
break;
|
||||
case ForceSetPasswordReason.TdeOffboarding:
|
||||
this.formPromise = this.updateTdeOffboardingPassword(masterPasswordHash, userKey);
|
||||
break;
|
||||
}
|
||||
|
||||
await this.formPromise;
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("updatedMasterPassword"),
|
||||
});
|
||||
|
||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
await this.masterPasswordService.setForceSetPasswordReason(
|
||||
ForceSetPasswordReason.None,
|
||||
userId,
|
||||
);
|
||||
|
||||
if (this.onSuccessfulChangePassword != null) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.onSuccessfulChangePassword();
|
||||
} else {
|
||||
this.messagingService.send("logout");
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
private async updateTempPassword(masterPasswordHash: string, userKey: [UserKey, EncString]) {
|
||||
const request = new UpdateTempPasswordRequest();
|
||||
request.key = userKey[1].encryptedString;
|
||||
request.newMasterPasswordHash = masterPasswordHash;
|
||||
request.masterPasswordHint = this.hint;
|
||||
|
||||
return this.masterPasswordApiService.putUpdateTempPassword(request);
|
||||
}
|
||||
|
||||
private async updatePassword(newMasterPasswordHash: string, userKey: [UserKey, EncString]) {
|
||||
const request = await this.userVerificationService.buildRequest(
|
||||
this.verification,
|
||||
PasswordRequest,
|
||||
);
|
||||
request.masterPasswordHint = this.hint;
|
||||
request.newMasterPasswordHash = newMasterPasswordHash;
|
||||
request.key = userKey[1].encryptedString;
|
||||
|
||||
return this.masterPasswordApiService.postPassword(request);
|
||||
}
|
||||
|
||||
private async updateTdeOffboardingPassword(
|
||||
masterPasswordHash: string,
|
||||
userKey: [UserKey, EncString],
|
||||
) {
|
||||
const request = new UpdateTdeOffboardingPasswordRequest();
|
||||
request.key = userKey[1].encryptedString;
|
||||
request.newMasterPasswordHash = masterPasswordHash;
|
||||
request.masterPasswordHint = this.hint;
|
||||
|
||||
return this.masterPasswordApiService.putUpdateTdeOffboardingPassword(request);
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,12 @@ import { CommonModule } from "@angular/common";
|
||||
import { Component, Input } from "@angular/core";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { LoginApprovalComponent } from "@bitwarden/auth/angular";
|
||||
import { DevicePendingAuthRequest } from "@bitwarden/common/auth/abstractions/devices/responses/device.response";
|
||||
import { BadgeModule, DialogService, ItemModule } from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
import { LoginApprovalDialogComponent } from "../login-approval/login-approval-dialog.component";
|
||||
|
||||
import { DeviceDisplayData } from "./device-management.component";
|
||||
import { clearAuthRequestAndResortDevices } from "./resort-devices.helper";
|
||||
|
||||
@@ -29,7 +28,7 @@ export class DeviceManagementItemGroupComponent {
|
||||
return;
|
||||
}
|
||||
|
||||
const loginApprovalDialog = LoginApprovalComponent.open(this.dialogService, {
|
||||
const loginApprovalDialog = LoginApprovalDialogComponent.open(this.dialogService, {
|
||||
notificationId: pendingAuthRequest.id,
|
||||
});
|
||||
|
||||
|
||||
@@ -3,9 +3,6 @@ import { Component, Input, OnChanges, SimpleChanges } from "@angular/core";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { LoginApprovalComponent } from "@bitwarden/auth/angular";
|
||||
import { DevicePendingAuthRequest } from "@bitwarden/common/auth/abstractions/devices/responses/device.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import {
|
||||
@@ -17,6 +14,8 @@ import {
|
||||
TableModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { LoginApprovalDialogComponent } from "../login-approval/login-approval-dialog.component";
|
||||
|
||||
import { DeviceDisplayData } from "./device-management.component";
|
||||
import { clearAuthRequestAndResortDevices } from "./resort-devices.helper";
|
||||
|
||||
@@ -68,7 +67,7 @@ export class DeviceManagementTableComponent implements OnChanges {
|
||||
return;
|
||||
}
|
||||
|
||||
const loginApprovalDialog = LoginApprovalComponent.open(this.dialogService, {
|
||||
const loginApprovalDialog = LoginApprovalDialogComponent.open(this.dialogService, {
|
||||
notificationId: pendingAuthRequest.id,
|
||||
});
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
</button>
|
||||
|
||||
<bit-popover [title]="'whatIsADevice' | i18n" #infoPopover>
|
||||
<p>{{ "aDeviceIs" | i18n }}</p>
|
||||
<p class="tw-mb-0">{{ "aDeviceIs" | i18n }}</p>
|
||||
</bit-popover>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -68,10 +68,7 @@ describe("AuthGuard", () => {
|
||||
{ path: "", component: EmptyComponent },
|
||||
{ path: "guarded-route", component: EmptyComponent, canActivate: [authGuard] },
|
||||
{ path: "lock", component: EmptyComponent },
|
||||
{ path: "set-password", component: EmptyComponent },
|
||||
{ path: "set-password-jit", component: EmptyComponent },
|
||||
{ path: "set-initial-password", component: EmptyComponent, canActivate: [authGuard] },
|
||||
{ path: "update-temp-password", component: EmptyComponent, canActivate: [authGuard] },
|
||||
{ path: "change-password", component: EmptyComponent },
|
||||
{ path: "remove-password", component: EmptyComponent, canActivate: [authGuard] },
|
||||
]),
|
||||
@@ -125,109 +122,58 @@ describe("AuthGuard", () => {
|
||||
});
|
||||
|
||||
describe("given user is Locked", () => {
|
||||
describe("given the PM16117_SetInitialPasswordRefactor feature flag is ON", () => {
|
||||
it("should redirect to /set-initial-password when the user has ForceSetPasswordReaason.TdeOffboardingUntrustedDevice", async () => {
|
||||
const { router } = setup(
|
||||
AuthenticationStatus.Locked,
|
||||
ForceSetPasswordReason.TdeOffboardingUntrustedDevice,
|
||||
false,
|
||||
FeatureFlag.PM16117_SetInitialPasswordRefactor,
|
||||
);
|
||||
it("should redirect to /set-initial-password when the user has ForceSetPasswordReaason.TdeOffboardingUntrustedDevice", async () => {
|
||||
const { router } = setup(
|
||||
AuthenticationStatus.Locked,
|
||||
ForceSetPasswordReason.TdeOffboardingUntrustedDevice,
|
||||
false,
|
||||
);
|
||||
|
||||
await router.navigate(["guarded-route"]);
|
||||
expect(router.url).toBe("/set-initial-password");
|
||||
});
|
||||
await router.navigate(["guarded-route"]);
|
||||
expect(router.url).toBe("/set-initial-password");
|
||||
});
|
||||
|
||||
it("should allow navigation to continue to /set-initial-password when the user has ForceSetPasswordReason.TdeOffboardingUntrustedDevice", async () => {
|
||||
const { router } = setup(
|
||||
AuthenticationStatus.Unlocked,
|
||||
ForceSetPasswordReason.TdeOffboardingUntrustedDevice,
|
||||
false,
|
||||
FeatureFlag.PM16117_SetInitialPasswordRefactor,
|
||||
);
|
||||
it("should allow navigation to continue to /set-initial-password when the user has ForceSetPasswordReason.TdeOffboardingUntrustedDevice", async () => {
|
||||
const { router } = setup(
|
||||
AuthenticationStatus.Unlocked,
|
||||
ForceSetPasswordReason.TdeOffboardingUntrustedDevice,
|
||||
false,
|
||||
);
|
||||
|
||||
await router.navigate(["/set-initial-password"]);
|
||||
expect(router.url).toContain("/set-initial-password");
|
||||
});
|
||||
await router.navigate(["/set-initial-password"]);
|
||||
expect(router.url).toContain("/set-initial-password");
|
||||
});
|
||||
});
|
||||
|
||||
describe("given user is Unlocked", () => {
|
||||
describe("given the PM16117_SetInitialPasswordRefactor feature flag is ON", () => {
|
||||
const tests = [
|
||||
ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission,
|
||||
ForceSetPasswordReason.TdeOffboarding,
|
||||
];
|
||||
describe("given user is Unlocked and ForceSetPasswordReason requires setting an initial password", () => {
|
||||
const tests = [
|
||||
ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission,
|
||||
ForceSetPasswordReason.TdeOffboarding,
|
||||
];
|
||||
|
||||
describe("given user attempts to navigate to an auth guarded route", () => {
|
||||
tests.forEach((reason) => {
|
||||
it(`should redirect to /set-initial-password when the user has ForceSetPasswordReason.${ForceSetPasswordReason[reason]}`, async () => {
|
||||
const { router } = setup(
|
||||
AuthenticationStatus.Unlocked,
|
||||
reason,
|
||||
false,
|
||||
FeatureFlag.PM16117_SetInitialPasswordRefactor,
|
||||
);
|
||||
describe("given user attempts to navigate to an auth guarded route", () => {
|
||||
tests.forEach((reason) => {
|
||||
it(`should redirect to /set-initial-password when the user has ForceSetPasswordReason.${ForceSetPasswordReason[reason]}`, async () => {
|
||||
const { router } = setup(AuthenticationStatus.Unlocked, reason, false);
|
||||
|
||||
await router.navigate(["guarded-route"]);
|
||||
expect(router.url).toContain("/set-initial-password");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("given user attempts to navigate to /set-initial-password", () => {
|
||||
tests.forEach((reason) => {
|
||||
it(`should allow navigation to continue to /set-initial-password when the user has ForceSetPasswordReason.${ForceSetPasswordReason[reason]}`, async () => {
|
||||
const { router } = setup(
|
||||
AuthenticationStatus.Unlocked,
|
||||
reason,
|
||||
false,
|
||||
FeatureFlag.PM16117_SetInitialPasswordRefactor,
|
||||
);
|
||||
|
||||
await router.navigate(["/set-initial-password"]);
|
||||
expect(router.url).toContain("/set-initial-password");
|
||||
});
|
||||
await router.navigate(["guarded-route"]);
|
||||
expect(router.url).toContain("/set-initial-password");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("given the PM16117_SetInitialPasswordRefactor feature flag is OFF", () => {
|
||||
const tests = [
|
||||
{
|
||||
reason: ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission,
|
||||
url: "/set-password",
|
||||
},
|
||||
{
|
||||
reason: ForceSetPasswordReason.TdeOffboarding,
|
||||
url: "/update-temp-password",
|
||||
},
|
||||
];
|
||||
describe("given user attempts to navigate to /set-initial-password", () => {
|
||||
tests.forEach((reason) => {
|
||||
it(`should allow navigation to continue to /set-initial-password when the user has ForceSetPasswordReason.${ForceSetPasswordReason[reason]}`, async () => {
|
||||
const { router } = setup(AuthenticationStatus.Unlocked, reason, false);
|
||||
|
||||
describe("given user attempts to navigate to an auth guarded route", () => {
|
||||
tests.forEach(({ reason, url }) => {
|
||||
it(`should redirect to ${url} when user has ForceSetPasswordReason.${ForceSetPasswordReason[reason]}`, async () => {
|
||||
const { router } = setup(AuthenticationStatus.Unlocked, reason);
|
||||
|
||||
await router.navigate(["/guarded-route"]);
|
||||
expect(router.url).toContain(url);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("given user attempts to navigate to the set- or update- password route itself", () => {
|
||||
tests.forEach(({ reason, url }) => {
|
||||
it(`should allow navigation to continue to ${url} when user has ForceSetPasswordReason.${ForceSetPasswordReason[reason]}`, async () => {
|
||||
const { router } = setup(AuthenticationStatus.Unlocked, reason);
|
||||
|
||||
await router.navigate([url]);
|
||||
expect(router.url).toContain(url);
|
||||
});
|
||||
await router.navigate(["/set-initial-password"]);
|
||||
expect(router.url).toContain("/set-initial-password");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("given the PM16117_ChangeExistingPasswordRefactor feature flag is ON", () => {
|
||||
describe("given user is Unlocked and ForceSetPasswordReason requires changing an existing password", () => {
|
||||
const tests = [
|
||||
ForceSetPasswordReason.AdminForcePasswordReset,
|
||||
ForceSetPasswordReason.WeakMasterPassword,
|
||||
@@ -236,12 +182,7 @@ describe("AuthGuard", () => {
|
||||
describe("given user attempts to navigate to an auth guarded route", () => {
|
||||
tests.forEach((reason) => {
|
||||
it(`should redirect to /change-password when user has ForceSetPasswordReason.${ForceSetPasswordReason[reason]}`, async () => {
|
||||
const { router } = setup(
|
||||
AuthenticationStatus.Unlocked,
|
||||
reason,
|
||||
false,
|
||||
FeatureFlag.PM16117_ChangeExistingPasswordRefactor,
|
||||
);
|
||||
const { router } = setup(AuthenticationStatus.Unlocked, reason, false);
|
||||
|
||||
await router.navigate(["guarded-route"]);
|
||||
expect(router.url).toContain("/change-password");
|
||||
@@ -256,7 +197,6 @@ describe("AuthGuard", () => {
|
||||
AuthenticationStatus.Unlocked,
|
||||
ForceSetPasswordReason.AdminForcePasswordReset,
|
||||
false,
|
||||
FeatureFlag.PM16117_ChangeExistingPasswordRefactor,
|
||||
);
|
||||
|
||||
await router.navigate(["/change-password"]);
|
||||
@@ -265,34 +205,5 @@ describe("AuthGuard", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("given the PM16117_ChangeExistingPasswordRefactor feature flag is OFF", () => {
|
||||
const tests = [
|
||||
ForceSetPasswordReason.AdminForcePasswordReset,
|
||||
ForceSetPasswordReason.WeakMasterPassword,
|
||||
];
|
||||
|
||||
describe("given user attempts to navigate to an auth guarded route", () => {
|
||||
tests.forEach((reason) => {
|
||||
it(`should redirect to /update-temp-password when user has ForceSetPasswordReason.${ForceSetPasswordReason[reason]}`, async () => {
|
||||
const { router } = setup(AuthenticationStatus.Unlocked, reason);
|
||||
|
||||
await router.navigate(["guarded-route"]);
|
||||
expect(router.url).toContain("/update-temp-password");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("given user attempts to navigate to /update-temp-password", () => {
|
||||
tests.forEach((reason) => {
|
||||
it(`should allow navigation to continue to /update-temp-password when user has ForceSetPasswordReason.${ForceSetPasswordReason[reason]}`, async () => {
|
||||
const { router } = setup(AuthenticationStatus.Unlocked, reason);
|
||||
|
||||
await router.navigate(["/update-temp-password"]);
|
||||
expect(router.url).toContain("/update-temp-password");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,10 +14,8 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
|
||||
import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
|
||||
export const authGuard: CanActivateFn = async (
|
||||
@@ -30,7 +28,6 @@ export const authGuard: CanActivateFn = async (
|
||||
const keyConnectorService = inject(KeyConnectorService);
|
||||
const accountService = inject(AccountService);
|
||||
const masterPasswordService = inject(MasterPasswordServiceAbstraction);
|
||||
const configService = inject(ConfigService);
|
||||
|
||||
const authStatus = await authService.getAuthStatus();
|
||||
|
||||
@@ -44,16 +41,11 @@ export const authGuard: CanActivateFn = async (
|
||||
masterPasswordService.forceSetPasswordReason$(userId),
|
||||
);
|
||||
|
||||
const isSetInitialPasswordFlagOn = await configService.getFeatureFlag(
|
||||
FeatureFlag.PM16117_SetInitialPasswordRefactor,
|
||||
);
|
||||
|
||||
// User JIT provisioned into a master-password-encryption org
|
||||
if (
|
||||
authStatus === AuthenticationStatus.Locked &&
|
||||
forceSetPasswordReason === ForceSetPasswordReason.SsoNewJitProvisionedUser &&
|
||||
!routerState.url.includes("set-initial-password") &&
|
||||
isSetInitialPasswordFlagOn
|
||||
!routerState.url.includes("set-initial-password")
|
||||
) {
|
||||
return router.createUrlTree(["/set-initial-password"]);
|
||||
}
|
||||
@@ -62,8 +54,7 @@ export const authGuard: CanActivateFn = async (
|
||||
if (
|
||||
authStatus === AuthenticationStatus.Locked &&
|
||||
forceSetPasswordReason === ForceSetPasswordReason.TdeOffboardingUntrustedDevice &&
|
||||
!routerState.url.includes("set-initial-password") &&
|
||||
isSetInitialPasswordFlagOn
|
||||
!routerState.url.includes("set-initial-password")
|
||||
) {
|
||||
return router.createUrlTree(["/set-initial-password"]);
|
||||
}
|
||||
@@ -90,39 +81,28 @@ export const authGuard: CanActivateFn = async (
|
||||
return router.createUrlTree(["/remove-password"]);
|
||||
}
|
||||
|
||||
// TDE org user has "manage account recovery" permission
|
||||
// Handle cases where a user needs to set a password when they don't already have one:
|
||||
// - TDE org user has been given "manage account recovery" permission
|
||||
// - TDE offboarding on a trusted device, where we have access to their encryption key wrap with their new password
|
||||
if (
|
||||
forceSetPasswordReason ===
|
||||
ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission &&
|
||||
!routerState.url.includes("set-password") &&
|
||||
(forceSetPasswordReason ===
|
||||
ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission ||
|
||||
forceSetPasswordReason === ForceSetPasswordReason.TdeOffboarding) &&
|
||||
!routerState.url.includes("set-initial-password")
|
||||
) {
|
||||
const route = isSetInitialPasswordFlagOn ? "/set-initial-password" : "/set-password";
|
||||
const route = "/set-initial-password";
|
||||
return router.createUrlTree([route]);
|
||||
}
|
||||
|
||||
// TDE Offboarding on trusted device
|
||||
if (
|
||||
forceSetPasswordReason === ForceSetPasswordReason.TdeOffboarding &&
|
||||
!routerState.url.includes("update-temp-password") &&
|
||||
!routerState.url.includes("set-initial-password")
|
||||
) {
|
||||
const route = isSetInitialPasswordFlagOn ? "/set-initial-password" : "/update-temp-password";
|
||||
return router.createUrlTree([route]);
|
||||
}
|
||||
|
||||
const isChangePasswordFlagOn = await configService.getFeatureFlag(
|
||||
FeatureFlag.PM16117_ChangeExistingPasswordRefactor,
|
||||
);
|
||||
|
||||
// Post- Account Recovery or Weak Password on login
|
||||
// Handle cases where a user has a password but needs to set a new one:
|
||||
// - Account recovery
|
||||
// - Weak Password on login
|
||||
if (
|
||||
(forceSetPasswordReason === ForceSetPasswordReason.AdminForcePasswordReset ||
|
||||
forceSetPasswordReason === ForceSetPasswordReason.WeakMasterPassword) &&
|
||||
!routerState.url.includes("update-temp-password") &&
|
||||
!routerState.url.includes("change-password")
|
||||
) {
|
||||
const route = isChangePasswordFlagOn ? "/change-password" : "/update-temp-password";
|
||||
const route = "/change-password";
|
||||
return router.createUrlTree([route]);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ import { lockGuard } from "./lock.guard";
|
||||
interface SetupParams {
|
||||
authStatus: AuthenticationStatus;
|
||||
canLock?: boolean;
|
||||
isLegacyUser?: boolean;
|
||||
clientType?: ClientType;
|
||||
everHadUserKey?: boolean;
|
||||
supportsDeviceTrust?: boolean;
|
||||
@@ -43,7 +42,6 @@ describe("lockGuard", () => {
|
||||
vaultTimeoutSettingsService.canLock.mockResolvedValue(setupParams.canLock);
|
||||
|
||||
const keyService: MockProxy<KeyService> = mock<KeyService>();
|
||||
keyService.isLegacyUser.mockResolvedValue(setupParams.isLegacyUser);
|
||||
keyService.everHadUserKey$.mockReturnValue(of(setupParams.everHadUserKey));
|
||||
|
||||
const platformUtilService: MockProxy<PlatformUtilsService> = mock<PlatformUtilsService>();
|
||||
@@ -155,37 +153,10 @@ describe("lockGuard", () => {
|
||||
expect(router.url).toBe("/");
|
||||
});
|
||||
|
||||
it("should log user out if they are a legacy user on a desktop client", async () => {
|
||||
const { router, messagingService } = setup({
|
||||
authStatus: AuthenticationStatus.Locked,
|
||||
canLock: true,
|
||||
isLegacyUser: true,
|
||||
clientType: ClientType.Desktop,
|
||||
});
|
||||
|
||||
await router.navigate(["lock"]);
|
||||
expect(router.url).toBe("/");
|
||||
expect(messagingService.send).toHaveBeenCalledWith("logout");
|
||||
});
|
||||
|
||||
it("should log user out if they are a legacy user on a browser extension client", async () => {
|
||||
const { router, messagingService } = setup({
|
||||
authStatus: AuthenticationStatus.Locked,
|
||||
canLock: true,
|
||||
isLegacyUser: true,
|
||||
clientType: ClientType.Browser,
|
||||
});
|
||||
|
||||
await router.navigate(["lock"]);
|
||||
expect(router.url).toBe("/");
|
||||
expect(messagingService.send).toHaveBeenCalledWith("logout");
|
||||
});
|
||||
|
||||
it("should allow navigation to the lock route when device trust is supported, the user has a MP, and the user is coming from the login-initiated page", async () => {
|
||||
const { router } = setup({
|
||||
authStatus: AuthenticationStatus.Locked,
|
||||
canLock: true,
|
||||
isLegacyUser: false,
|
||||
clientType: ClientType.Web,
|
||||
everHadUserKey: false,
|
||||
supportsDeviceTrust: true,
|
||||
@@ -213,7 +184,6 @@ describe("lockGuard", () => {
|
||||
const { router } = setup({
|
||||
authStatus: AuthenticationStatus.Locked,
|
||||
canLock: true,
|
||||
isLegacyUser: false,
|
||||
clientType: ClientType.Web,
|
||||
everHadUserKey: false,
|
||||
supportsDeviceTrust: true,
|
||||
|
||||
@@ -13,7 +13,6 @@ import { UserVerificationService } from "@bitwarden/common/auth/abstractions/use
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
/**
|
||||
@@ -31,7 +30,6 @@ export function lockGuard(): CanActivateFn {
|
||||
const authService = inject(AuthService);
|
||||
const keyService = inject(KeyService);
|
||||
const deviceTrustService = inject(DeviceTrustServiceAbstraction);
|
||||
const messagingService = inject(MessagingService);
|
||||
const router = inject(Router);
|
||||
const userVerificationService = inject(UserVerificationService);
|
||||
const vaultTimeoutSettingsService = inject(VaultTimeoutSettingsService);
|
||||
@@ -56,11 +54,6 @@ export function lockGuard(): CanActivateFn {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (await keyService.isLegacyUser()) {
|
||||
messagingService.send("logout");
|
||||
return false;
|
||||
}
|
||||
|
||||
// User is authN and in locked state.
|
||||
|
||||
const tdeEnabled = await firstValueFrom(deviceTrustService.supportsDeviceTrust$);
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
|
||||
import { DefaultLoginApprovalDialogComponentService } from "./default-login-approval-dialog-component.service";
|
||||
import { LoginApprovalDialogComponent } from "./login-approval-dialog.component";
|
||||
|
||||
describe("DefaultLoginApprovalDialogComponentService", () => {
|
||||
let service: DefaultLoginApprovalDialogComponentService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [DefaultLoginApprovalDialogComponentService],
|
||||
});
|
||||
|
||||
service = TestBed.inject(DefaultLoginApprovalDialogComponentService);
|
||||
});
|
||||
|
||||
it("is created successfully", () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
it("has showLoginRequestedAlertIfWindowNotVisible method that is a no-op", async () => {
|
||||
const loginApprovalDialogComponent = {} as LoginApprovalDialogComponent;
|
||||
|
||||
const result = await service.showLoginRequestedAlertIfWindowNotVisible(
|
||||
loginApprovalDialogComponent.email,
|
||||
);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import { LoginApprovalDialogComponentServiceAbstraction } from "./login-approval-dialog-component.service.abstraction";
|
||||
|
||||
/**
|
||||
* Default implementation of the LoginApprovalDialogComponentServiceAbstraction.
|
||||
*/
|
||||
export class DefaultLoginApprovalDialogComponentService
|
||||
implements LoginApprovalDialogComponentServiceAbstraction
|
||||
{
|
||||
/**
|
||||
* No-op implementation of the showLoginRequestedAlertIfWindowNotVisible method.
|
||||
* @returns
|
||||
*/
|
||||
async showLoginRequestedAlertIfWindowNotVisible(email?: string): Promise<void> {
|
||||
return;
|
||||
}
|
||||
}
|
||||
3
libs/angular/src/auth/login-approval/index.ts
Normal file
3
libs/angular/src/auth/login-approval/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from "./login-approval-dialog.component";
|
||||
export * from "./login-approval-dialog-component.service.abstraction";
|
||||
export * from "./default-login-approval-dialog-component.service";
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Abstraction for the LoginApprovalDialogComponent service.
|
||||
*/
|
||||
export abstract class LoginApprovalDialogComponentServiceAbstraction {
|
||||
/**
|
||||
* Shows a login requested alert if the window is not visible.
|
||||
*/
|
||||
abstract showLoginRequestedAlertIfWindowNotVisible: (email?: string) => Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<bit-dialog>
|
||||
<span bitDialogTitle>{{ "loginRequest" | i18n }}</span>
|
||||
|
||||
<ng-container bitDialogContent>
|
||||
<ng-container *ngIf="loading">
|
||||
<div class="tw-flex tw-items-center tw-justify-center" *ngIf="loading">
|
||||
<i class="bwi bwi-spinner bwi-spin bwi-3x" aria-hidden="true"></i>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="!loading">
|
||||
<p>{{ "accessAttemptBy" | i18n: email }}</p>
|
||||
<div>
|
||||
<span class="tw-font-medium">{{ "fingerprintPhraseHeader" | i18n }}</span>
|
||||
<p class="tw-text-code">{{ fingerprintPhrase }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="tw-font-medium">{{ "deviceType" | i18n }}</span>
|
||||
<p>{{ readableDeviceTypeName }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="tw-font-medium">{{ "location" | i18n }}</span>
|
||||
<p>
|
||||
<span class="tw-capitalize">{{ authRequestResponse?.requestCountryName }} </span>
|
||||
({{ authRequestResponse?.requestIpAddress }})
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="tw-font-medium">{{ "time" | i18n }}</span>
|
||||
<p>{{ requestTimeText }}</p>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-container bitDialogFooter>
|
||||
<button
|
||||
bitButton
|
||||
type="button"
|
||||
buttonType="primary"
|
||||
[bitAction]="approveLogin"
|
||||
[disabled]="loading"
|
||||
>
|
||||
{{ "confirmAccess" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
bitButton
|
||||
type="button"
|
||||
buttonType="secondary"
|
||||
[bitAction]="denyLogin"
|
||||
[disabled]="loading"
|
||||
>
|
||||
{{ "denyAccess" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
@@ -0,0 +1,126 @@
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction";
|
||||
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { DialogRef, DIALOG_DATA, ToastService } from "@bitwarden/components";
|
||||
import { LogService } from "@bitwarden/logging";
|
||||
|
||||
import { LoginApprovalDialogComponentServiceAbstraction } from "./login-approval-dialog-component.service.abstraction";
|
||||
import { LoginApprovalDialogComponent } from "./login-approval-dialog.component";
|
||||
|
||||
describe("LoginApprovalDialogComponent", () => {
|
||||
let component: LoginApprovalDialogComponent;
|
||||
let fixture: ComponentFixture<LoginApprovalDialogComponent>;
|
||||
|
||||
let accountService: MockProxy<AccountService>;
|
||||
let apiService: MockProxy<ApiService>;
|
||||
let authRequestService: MockProxy<AuthRequestServiceAbstraction>;
|
||||
let devicesService: MockProxy<DevicesServiceAbstraction>;
|
||||
let dialogRef: MockProxy<DialogRef>;
|
||||
let i18nService: MockProxy<I18nService>;
|
||||
let logService: MockProxy<LogService>;
|
||||
let toastService: MockProxy<ToastService>;
|
||||
let validationService: MockProxy<ValidationService>;
|
||||
|
||||
const testNotificationId = "test-notification-id";
|
||||
const testEmail = "test@bitwarden.com";
|
||||
const testPublicKey = "test-public-key";
|
||||
|
||||
beforeEach(async () => {
|
||||
accountService = mock<AccountService>();
|
||||
apiService = mock<ApiService>();
|
||||
authRequestService = mock<AuthRequestServiceAbstraction>();
|
||||
devicesService = mock<DevicesServiceAbstraction>();
|
||||
dialogRef = mock<DialogRef>();
|
||||
i18nService = mock<I18nService>();
|
||||
logService = mock<LogService>();
|
||||
toastService = mock<ToastService>();
|
||||
validationService = mock<ValidationService>();
|
||||
|
||||
accountService.activeAccount$ = of({
|
||||
email: testEmail,
|
||||
id: "test-user-id" as UserId,
|
||||
emailVerified: true,
|
||||
name: null,
|
||||
});
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [LoginApprovalDialogComponent],
|
||||
providers: [
|
||||
{ provide: DIALOG_DATA, useValue: { notificationId: testNotificationId } },
|
||||
{ provide: AccountService, useValue: accountService },
|
||||
{ provide: ApiService, useValue: apiService },
|
||||
{ provide: AuthRequestServiceAbstraction, useValue: authRequestService },
|
||||
{ provide: DevicesServiceAbstraction, useValue: devicesService },
|
||||
{ provide: DialogRef, useValue: dialogRef },
|
||||
{ provide: I18nService, useValue: i18nService },
|
||||
{ provide: LogService, useValue: logService },
|
||||
{ provide: ToastService, useValue: toastService },
|
||||
{ provide: ValidationService, useValue: validationService },
|
||||
{
|
||||
provide: LoginApprovalDialogComponentServiceAbstraction,
|
||||
useValue: mock<LoginApprovalDialogComponentServiceAbstraction>(),
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(LoginApprovalDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
});
|
||||
|
||||
it("creates successfully", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
describe("ngOnInit", () => {
|
||||
beforeEach(() => {
|
||||
apiService.getAuthRequest.mockResolvedValue({
|
||||
publicKey: testPublicKey,
|
||||
creationDate: new Date().toISOString(),
|
||||
} as AuthRequestResponse);
|
||||
authRequestService.getFingerprintPhrase.mockResolvedValue("test-phrase");
|
||||
});
|
||||
|
||||
it("retrieves and sets auth request data", async () => {
|
||||
await component.ngOnInit();
|
||||
|
||||
expect(apiService.getAuthRequest).toHaveBeenCalledWith(testNotificationId);
|
||||
expect(component.email).toBe(testEmail);
|
||||
expect(component.fingerprintPhrase).toBeDefined();
|
||||
});
|
||||
|
||||
it("updates time text initially", async () => {
|
||||
i18nService.t.mockReturnValue("justNow");
|
||||
|
||||
await component.ngOnInit();
|
||||
expect(component.requestTimeText).toBe("justNow");
|
||||
});
|
||||
});
|
||||
|
||||
describe("denyLogin", () => {
|
||||
it("denies auth request and shows info toast", async () => {
|
||||
const response = { requestApproved: false } as AuthRequestResponse;
|
||||
apiService.getAuthRequest.mockResolvedValue(response);
|
||||
authRequestService.approveOrDenyAuthRequest.mockResolvedValue(response);
|
||||
i18nService.t.mockReturnValue("denied message");
|
||||
|
||||
await component.denyLogin();
|
||||
|
||||
expect(authRequestService.approveOrDenyAuthRequest).toHaveBeenCalledWith(false, response);
|
||||
expect(toastService.showToast).toHaveBeenCalledWith({
|
||||
variant: "info",
|
||||
message: "denied message",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,220 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, OnInit, OnDestroy, Inject } from "@angular/core";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction";
|
||||
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import {
|
||||
DIALOG_DATA,
|
||||
DialogRef,
|
||||
AsyncActionsModule,
|
||||
ButtonModule,
|
||||
DialogModule,
|
||||
DialogService,
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
import { LogService } from "@bitwarden/logging";
|
||||
|
||||
import { LoginApprovalDialogComponentServiceAbstraction } from "./login-approval-dialog-component.service.abstraction";
|
||||
|
||||
const RequestTimeOut = 60000 * 15; // 15 Minutes
|
||||
const RequestTimeUpdate = 60000 * 5; // 5 Minutes
|
||||
|
||||
export interface LoginApprovalDialogParams {
|
||||
notificationId: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
templateUrl: "login-approval-dialog.component.html",
|
||||
imports: [AsyncActionsModule, ButtonModule, CommonModule, DialogModule, JslibModule],
|
||||
})
|
||||
export class LoginApprovalDialogComponent implements OnInit, OnDestroy {
|
||||
authRequestId: string;
|
||||
authRequestResponse?: AuthRequestResponse;
|
||||
email?: string;
|
||||
fingerprintPhrase?: string;
|
||||
interval?: NodeJS.Timeout;
|
||||
loading = true;
|
||||
readableDeviceTypeName?: string;
|
||||
requestTimeText?: string;
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) private params: LoginApprovalDialogParams,
|
||||
private accountService: AccountService,
|
||||
private apiService: ApiService,
|
||||
private authRequestService: AuthRequestServiceAbstraction,
|
||||
private devicesService: DevicesServiceAbstraction,
|
||||
private dialogRef: DialogRef,
|
||||
private i18nService: I18nService,
|
||||
private loginApprovalDialogComponentService: LoginApprovalDialogComponentServiceAbstraction,
|
||||
private logService: LogService,
|
||||
private toastService: ToastService,
|
||||
private validationService: ValidationService,
|
||||
) {
|
||||
this.authRequestId = params.notificationId;
|
||||
}
|
||||
|
||||
async ngOnDestroy(): Promise<void> {
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
if (this.authRequestId == null) {
|
||||
this.logService.error("LoginApprovalDialogComponent: authRequestId is null");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.authRequestResponse = await this.apiService.getAuthRequest(this.authRequestId);
|
||||
} catch (error) {
|
||||
this.validationService.showError(error);
|
||||
this.logService.error("LoginApprovalDialogComponent: getAuthRequest error", error);
|
||||
}
|
||||
|
||||
if (this.authRequestResponse == null) {
|
||||
this.logService.error("LoginApprovalDialogComponent: authRequestResponse not found");
|
||||
return;
|
||||
}
|
||||
|
||||
const publicKey = Utils.fromB64ToArray(this.authRequestResponse.publicKey);
|
||||
|
||||
this.email = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.email)),
|
||||
);
|
||||
|
||||
if (!this.email) {
|
||||
this.logService.error("LoginApprovalDialogComponent: email not found");
|
||||
return;
|
||||
}
|
||||
|
||||
this.fingerprintPhrase = await this.authRequestService.getFingerprintPhrase(
|
||||
this.email,
|
||||
publicKey,
|
||||
);
|
||||
|
||||
this.readableDeviceTypeName = this.devicesService.getReadableDeviceTypeName(
|
||||
this.authRequestResponse.requestDeviceTypeValue,
|
||||
);
|
||||
|
||||
this.updateTimeText();
|
||||
|
||||
this.interval = setInterval(() => {
|
||||
this.updateTimeText();
|
||||
}, RequestTimeUpdate);
|
||||
|
||||
await this.loginApprovalDialogComponentService.showLoginRequestedAlertIfWindowNotVisible(
|
||||
this.email,
|
||||
);
|
||||
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strongly-typed helper to open a LoginApprovalDialog
|
||||
* @param dialogService Instance of the dialog service that will be used to open the dialog
|
||||
* @param data Configuration for the dialog
|
||||
*/
|
||||
static open(dialogService: DialogService, data: LoginApprovalDialogParams) {
|
||||
return dialogService.open(LoginApprovalDialogComponent, { data });
|
||||
}
|
||||
|
||||
denyLogin = async () => {
|
||||
await this.retrieveAuthRequestAndRespond(false);
|
||||
};
|
||||
|
||||
approveLogin = async () => {
|
||||
await this.retrieveAuthRequestAndRespond(true);
|
||||
};
|
||||
|
||||
private async retrieveAuthRequestAndRespond(approve: boolean) {
|
||||
this.authRequestResponse = await this.apiService.getAuthRequest(this.authRequestId);
|
||||
if (this.authRequestResponse.requestApproved || this.authRequestResponse.responseDate != null) {
|
||||
this.toastService.showToast({
|
||||
variant: "info",
|
||||
message: this.i18nService.t("thisRequestIsNoLongerValid"),
|
||||
});
|
||||
} else {
|
||||
const loginResponse = await this.authRequestService.approveOrDenyAuthRequest(
|
||||
approve,
|
||||
this.authRequestResponse,
|
||||
);
|
||||
this.showResultToast(loginResponse);
|
||||
}
|
||||
|
||||
this.dialogRef.close(approve);
|
||||
}
|
||||
|
||||
showResultToast(loginResponse: AuthRequestResponse) {
|
||||
if (loginResponse.requestApproved) {
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
message: this.i18nService.t(
|
||||
"loginRequestApprovedForEmailOnDevice",
|
||||
this.email,
|
||||
this.devicesService.getReadableDeviceTypeName(loginResponse.requestDeviceTypeValue),
|
||||
),
|
||||
});
|
||||
} else {
|
||||
this.toastService.showToast({
|
||||
variant: "info",
|
||||
message: this.i18nService.t("youDeniedLoginAttemptFromAnotherDevice"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
updateTimeText() {
|
||||
if (this.authRequestResponse == null) {
|
||||
this.logService.error("LoginApprovalDialogComponent: authRequestResponse not found");
|
||||
return;
|
||||
}
|
||||
|
||||
const requestDate = new Date(this.authRequestResponse.creationDate);
|
||||
const requestDateUTC = Date.UTC(
|
||||
requestDate.getUTCFullYear(),
|
||||
requestDate.getUTCMonth(),
|
||||
requestDate.getDate(),
|
||||
requestDate.getUTCHours(),
|
||||
requestDate.getUTCMinutes(),
|
||||
requestDate.getUTCSeconds(),
|
||||
requestDate.getUTCMilliseconds(),
|
||||
);
|
||||
|
||||
const dateNow = new Date(Date.now());
|
||||
const dateNowUTC = Date.UTC(
|
||||
dateNow.getUTCFullYear(),
|
||||
dateNow.getUTCMonth(),
|
||||
dateNow.getDate(),
|
||||
dateNow.getUTCHours(),
|
||||
dateNow.getUTCMinutes(),
|
||||
dateNow.getUTCSeconds(),
|
||||
dateNow.getUTCMilliseconds(),
|
||||
);
|
||||
|
||||
const diffInMinutes = dateNowUTC - requestDateUTC;
|
||||
|
||||
if (diffInMinutes <= RequestTimeUpdate) {
|
||||
this.requestTimeText = this.i18nService.t("justNow");
|
||||
} else if (diffInMinutes < RequestTimeOut) {
|
||||
this.requestTimeText = this.i18nService.t(
|
||||
"requestedXMinutesAgo",
|
||||
(diffInMinutes / 60000).toFixed(),
|
||||
);
|
||||
} else {
|
||||
clearInterval(this.interval);
|
||||
this.dialogRef.close();
|
||||
this.toastService.showToast({
|
||||
variant: "info",
|
||||
message: this.i18nService.t("loginRequestHasAlreadyExpired"),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,7 +54,6 @@ import { UserTypePipe } from "./pipes/user-type.pipe";
|
||||
import { EllipsisPipe } from "./platform/pipes/ellipsis.pipe";
|
||||
import { FingerprintPipe } from "./platform/pipes/fingerprint.pipe";
|
||||
import { I18nPipe } from "./platform/pipes/i18n.pipe";
|
||||
import { PasswordStrengthComponent } from "./tools/password-strength/password-strength.component";
|
||||
import { IconComponent } from "./vault/components/icon.component";
|
||||
|
||||
@NgModule({
|
||||
@@ -108,7 +107,6 @@ import { IconComponent } from "./vault/components/icon.component";
|
||||
TrueFalseValueDirective,
|
||||
LaunchClickDirective,
|
||||
UserNamePipe,
|
||||
PasswordStrengthComponent,
|
||||
UserTypePipe,
|
||||
IfFeatureDirective,
|
||||
FingerprintPipe,
|
||||
@@ -143,7 +141,6 @@ import { IconComponent } from "./vault/components/icon.component";
|
||||
CopyClickDirective,
|
||||
LaunchClickDirective,
|
||||
UserNamePipe,
|
||||
PasswordStrengthComponent,
|
||||
UserTypePipe,
|
||||
IfFeatureDirective,
|
||||
FingerprintPipe,
|
||||
|
||||
@@ -18,17 +18,14 @@ import {
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import {
|
||||
DefaultLoginApprovalComponentService,
|
||||
DefaultLoginComponentService,
|
||||
DefaultLoginDecryptionOptionsService,
|
||||
DefaultRegistrationFinishService,
|
||||
DefaultSetPasswordJitService,
|
||||
DefaultTwoFactorAuthComponentService,
|
||||
DefaultTwoFactorAuthWebAuthnComponentService,
|
||||
LoginComponentService,
|
||||
LoginDecryptionOptionsService,
|
||||
RegistrationFinishService as RegistrationFinishServiceAbstraction,
|
||||
SetPasswordJitService,
|
||||
TwoFactorAuthComponentService,
|
||||
TwoFactorAuthWebAuthnComponentService,
|
||||
} from "@bitwarden/auth/angular";
|
||||
@@ -42,7 +39,6 @@ import {
|
||||
DefaultLoginSuccessHandlerService,
|
||||
DefaultLogoutService,
|
||||
InternalUserDecryptionOptionsServiceAbstraction,
|
||||
LoginApprovalComponentServiceAbstraction,
|
||||
LoginEmailService,
|
||||
LoginEmailServiceAbstraction,
|
||||
LoginStrategyService,
|
||||
@@ -50,8 +46,6 @@ import {
|
||||
LoginSuccessHandlerService,
|
||||
LogoutReason,
|
||||
LogoutService,
|
||||
PinService,
|
||||
PinServiceAbstraction,
|
||||
UserDecryptionOptionsService,
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
@@ -114,6 +108,7 @@ import { AnonymousHubService } from "@bitwarden/common/auth/services/anonymous-h
|
||||
import { AuthRequestAnsweringService } from "@bitwarden/common/auth/services/auth-request-answering/auth-request-answering.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
|
||||
import { AvatarService } from "@bitwarden/common/auth/services/avatar.service";
|
||||
import { DefaultActiveUserAccessor } from "@bitwarden/common/auth/services/default-active-user.accessor";
|
||||
import { DevicesServiceImplementation } from "@bitwarden/common/auth/services/devices/devices.service.implementation";
|
||||
import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation";
|
||||
import { MasterPasswordApiService } from "@bitwarden/common/auth/services/master-password/master-password-api.service.implementation";
|
||||
@@ -156,11 +151,9 @@ import { OrganizationBillingApiService } from "@bitwarden/common/billing/service
|
||||
import { OrganizationSponsorshipApiService } from "@bitwarden/common/billing/services/organization/organization-sponsorship-api.service";
|
||||
import { OrganizationBillingService } from "@bitwarden/common/billing/services/organization-billing.service";
|
||||
import { TaxService } from "@bitwarden/common/billing/services/tax.service";
|
||||
import { BulkEncryptService } from "@bitwarden/common/key-management/crypto/abstractions/bulk-encrypt.service";
|
||||
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { BulkEncryptServiceImplementation } from "@bitwarden/common/key-management/crypto/services/bulk-encrypt.service.implementation";
|
||||
import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/key-management/crypto/services/multithread-encrypt.service.implementation";
|
||||
import { EncryptServiceImplementation } from "@bitwarden/common/key-management/crypto/services/encrypt.service.implementation";
|
||||
import { WebCryptoFunctionService } from "@bitwarden/common/key-management/crypto/services/web-crypto-function.service";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||
import { DeviceTrustService } from "@bitwarden/common/key-management/device-trust/services/device-trust.service.implementation";
|
||||
@@ -171,6 +164,8 @@ import {
|
||||
MasterPasswordServiceAbstraction,
|
||||
} from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { MasterPasswordService } from "@bitwarden/common/key-management/master-password/services/master-password.service";
|
||||
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
|
||||
import { PinService } from "@bitwarden/common/key-management/pin/pin.service.implementation";
|
||||
import {
|
||||
SendPasswordService,
|
||||
DefaultSendPasswordService,
|
||||
@@ -219,7 +214,7 @@ import {
|
||||
WebPushConnectionService,
|
||||
WebPushNotificationsApiService,
|
||||
} from "@bitwarden/common/platform/notifications/internal";
|
||||
import { SystemNotificationsService } from "@bitwarden/common/platform/notifications/system-notifications-service";
|
||||
import { SystemNotificationsService } from "@bitwarden/common/platform/notifications/system-notifications.service";
|
||||
import { UnsupportedSystemNotificationsService } from "@bitwarden/common/platform/notifications/unsupported-system-notifications.service";
|
||||
import {
|
||||
DefaultTaskSchedulerService,
|
||||
@@ -242,6 +237,7 @@ import { StorageServiceProvider } from "@bitwarden/common/platform/services/stor
|
||||
import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/services/validation.service";
|
||||
import {
|
||||
ActiveUserAccessor,
|
||||
ActiveUserStateProvider,
|
||||
DerivedStateProvider,
|
||||
GlobalStateProvider,
|
||||
@@ -351,6 +347,8 @@ import {
|
||||
VaultExportServiceAbstraction,
|
||||
} from "@bitwarden/vault-export-core";
|
||||
|
||||
import { DefaultLoginApprovalDialogComponentService } from "../auth/login-approval/default-login-approval-dialog-component.service";
|
||||
import { LoginApprovalDialogComponentServiceAbstraction } from "../auth/login-approval/login-approval-dialog-component.service.abstraction";
|
||||
import { DefaultSetInitialPasswordService } from "../auth/password-management/set-initial-password/default-set-initial-password.service.implementation";
|
||||
import { SetInitialPasswordService } from "../auth/password-management/set-initial-password/set-initial-password.service.abstraction";
|
||||
import { DeviceTrustToastService as DeviceTrustToastServiceAbstraction } from "../auth/services/device-trust-toast.service.abstraction";
|
||||
@@ -539,13 +537,13 @@ const safeProviders: SafeProvider[] = [
|
||||
stateService: StateServiceAbstraction,
|
||||
autofillSettingsService: AutofillSettingsServiceAbstraction,
|
||||
encryptService: EncryptService,
|
||||
bulkEncryptService: BulkEncryptService,
|
||||
fileUploadService: CipherFileUploadServiceAbstraction,
|
||||
configService: ConfigService,
|
||||
stateProvider: StateProvider,
|
||||
accountService: AccountServiceAbstraction,
|
||||
logService: LogService,
|
||||
cipherEncryptionService: CipherEncryptionService,
|
||||
messagingService: MessagingServiceAbstraction,
|
||||
) =>
|
||||
new CipherService(
|
||||
keyService,
|
||||
@@ -556,13 +554,13 @@ const safeProviders: SafeProvider[] = [
|
||||
stateService,
|
||||
autofillSettingsService,
|
||||
encryptService,
|
||||
bulkEncryptService,
|
||||
fileUploadService,
|
||||
configService,
|
||||
stateProvider,
|
||||
accountService,
|
||||
logService,
|
||||
cipherEncryptionService,
|
||||
messagingService,
|
||||
),
|
||||
deps: [
|
||||
KeyService,
|
||||
@@ -573,13 +571,13 @@ const safeProviders: SafeProvider[] = [
|
||||
StateServiceAbstraction,
|
||||
AutofillSettingsServiceAbstraction,
|
||||
EncryptService,
|
||||
BulkEncryptService,
|
||||
CipherFileUploadServiceAbstraction,
|
||||
ConfigService,
|
||||
StateProvider,
|
||||
AccountServiceAbstraction,
|
||||
LogService,
|
||||
CipherEncryptionService,
|
||||
MessagingServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
@@ -1003,14 +1001,9 @@ const safeProviders: SafeProvider[] = [
|
||||
}),
|
||||
safeProvider({
|
||||
provide: EncryptService,
|
||||
useClass: MultithreadEncryptServiceImplementation,
|
||||
useClass: EncryptServiceImplementation,
|
||||
deps: [CryptoFunctionServiceAbstraction, LogService, LOG_MAC_FAILURES],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: BulkEncryptService,
|
||||
useClass: BulkEncryptServiceImplementation,
|
||||
deps: [CryptoFunctionServiceAbstraction, LogService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: EventUploadServiceAbstraction,
|
||||
useClass: EventUploadService,
|
||||
@@ -1057,6 +1050,8 @@ const safeProviders: SafeProvider[] = [
|
||||
KeyGenerationServiceAbstraction,
|
||||
EncryptService,
|
||||
LogService,
|
||||
CryptoFunctionServiceAbstraction,
|
||||
AccountServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
@@ -1315,10 +1310,15 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: DefaultGlobalStateProvider,
|
||||
deps: [StorageServiceProvider, LogService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: ActiveUserAccessor,
|
||||
useClass: DefaultActiveUserAccessor,
|
||||
deps: [AccountServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: ActiveUserStateProvider,
|
||||
useClass: DefaultActiveUserStateProvider,
|
||||
deps: [AccountServiceAbstraction, SingleUserStateProvider],
|
||||
deps: [ActiveUserAccessor, SingleUserStateProvider],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SingleUserStateProvider,
|
||||
@@ -1451,21 +1451,6 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: DefaultOrganizationInviteService,
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SetPasswordJitService,
|
||||
useClass: DefaultSetPasswordJitService,
|
||||
deps: [
|
||||
EncryptService,
|
||||
I18nServiceAbstraction,
|
||||
KdfConfigService,
|
||||
KeyService,
|
||||
MasterPasswordApiServiceAbstraction,
|
||||
InternalMasterPasswordServiceAbstraction,
|
||||
OrganizationApiServiceAbstraction,
|
||||
OrganizationUserApiService,
|
||||
InternalUserDecryptionOptionsServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SetInitialPasswordService,
|
||||
useClass: DefaultSetInitialPasswordService,
|
||||
@@ -1546,8 +1531,8 @@ const safeProviders: SafeProvider[] = [
|
||||
deps: [CryptoFunctionServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: LoginApprovalComponentServiceAbstraction,
|
||||
useClass: DefaultLoginApprovalComponentService,
|
||||
provide: LoginApprovalDialogComponentServiceAbstraction,
|
||||
useClass: DefaultLoginApprovalDialogComponentService,
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
<div class="progress">
|
||||
<div
|
||||
class="progress-bar {{ color }}"
|
||||
role="progressbar"
|
||||
[ngStyle]="{ width: scoreWidth + '%' }"
|
||||
attr.aria-valuenow="{{ scoreWidth }}"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
>
|
||||
<ng-container *ngIf="showText && text">
|
||||
{{ text }}
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,118 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, EventEmitter, Input, OnChanges, Output } from "@angular/core";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||
|
||||
export interface PasswordColorText {
|
||||
color: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated July 2024: Use new PasswordStrengthV2Component instead
|
||||
*/
|
||||
@Component({
|
||||
selector: "app-password-strength",
|
||||
templateUrl: "password-strength.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
export class PasswordStrengthComponent implements OnChanges {
|
||||
@Input() showText = false;
|
||||
@Input() email: string;
|
||||
@Input() name: string;
|
||||
@Input() set password(value: string) {
|
||||
this.updatePasswordStrength(value);
|
||||
}
|
||||
@Output() passwordStrengthResult = new EventEmitter<any>();
|
||||
@Output() passwordScoreColor = new EventEmitter<PasswordColorText>();
|
||||
|
||||
masterPasswordScore: number;
|
||||
scoreWidth = 0;
|
||||
color = "bg-danger";
|
||||
text: string;
|
||||
|
||||
private masterPasswordStrengthTimeout: any;
|
||||
|
||||
//used by desktop and browser to display strength text color
|
||||
get masterPasswordScoreColor() {
|
||||
switch (this.masterPasswordScore) {
|
||||
case 4:
|
||||
return "success";
|
||||
case 3:
|
||||
return "primary";
|
||||
case 2:
|
||||
return "warning";
|
||||
default:
|
||||
return "danger";
|
||||
}
|
||||
}
|
||||
|
||||
//used by desktop and browser to display strength text
|
||||
get masterPasswordScoreText() {
|
||||
switch (this.masterPasswordScore) {
|
||||
case 4:
|
||||
return this.i18nService.t("strong");
|
||||
case 3:
|
||||
return this.i18nService.t("good");
|
||||
case 2:
|
||||
return this.i18nService.t("weak");
|
||||
default:
|
||||
return this.masterPasswordScore != null ? this.i18nService.t("weak") : null;
|
||||
}
|
||||
}
|
||||
|
||||
constructor(
|
||||
private i18nService: I18nService,
|
||||
private passwordStrengthService: PasswordStrengthServiceAbstraction,
|
||||
) {}
|
||||
|
||||
ngOnChanges(): void {
|
||||
this.masterPasswordStrengthTimeout = setTimeout(() => {
|
||||
this.scoreWidth = this.masterPasswordScore == null ? 0 : (this.masterPasswordScore + 1) * 20;
|
||||
|
||||
switch (this.masterPasswordScore) {
|
||||
case 4:
|
||||
this.color = "bg-success";
|
||||
this.text = this.i18nService.t("strong");
|
||||
break;
|
||||
case 3:
|
||||
this.color = "bg-primary";
|
||||
this.text = this.i18nService.t("good");
|
||||
break;
|
||||
case 2:
|
||||
this.color = "bg-warning";
|
||||
this.text = this.i18nService.t("weak");
|
||||
break;
|
||||
default:
|
||||
this.color = "bg-danger";
|
||||
this.text = this.masterPasswordScore != null ? this.i18nService.t("weak") : null;
|
||||
break;
|
||||
}
|
||||
|
||||
this.setPasswordScoreText(this.color, this.text);
|
||||
}, 300);
|
||||
}
|
||||
|
||||
updatePasswordStrength(password: string) {
|
||||
const masterPassword = password;
|
||||
|
||||
if (this.masterPasswordStrengthTimeout != null) {
|
||||
clearTimeout(this.masterPasswordStrengthTimeout);
|
||||
}
|
||||
|
||||
const strengthResult = this.passwordStrengthService.getPasswordStrength(
|
||||
masterPassword,
|
||||
this.email,
|
||||
this.name?.trim().toLowerCase().split(" "),
|
||||
);
|
||||
this.passwordStrengthResult.emit(strengthResult);
|
||||
this.masterPasswordScore = strengthResult == null ? null : strengthResult.score;
|
||||
}
|
||||
|
||||
setPasswordScoreText(color: string, text: string) {
|
||||
color = color.slice(3);
|
||||
this.passwordScoreColor.emit({ color: color, text: text });
|
||||
}
|
||||
}
|
||||
@@ -63,7 +63,7 @@ export class FolderAddEditComponent implements OnInit {
|
||||
|
||||
try {
|
||||
const activeUserId = await firstValueFrom(this.activeUserId$);
|
||||
const userKey = await this.keyService.getUserKeyWithLegacySupport(activeUserId);
|
||||
const userKey = await this.keyService.getUserKey(activeUserId);
|
||||
const folder = await this.folderService.encrypt(this.folder, userKey);
|
||||
this.formPromise = this.folderApiService.save(folder, activeUserId);
|
||||
await this.formPromise;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div
|
||||
class="tw-rounded-2xl tw-bg-primary-100 tw-border-primary-600 tw-border-solid tw-border tw-p-4 tw-pt-3 tw-flex tw-flex-col tw-gap-2 tw-mb-4"
|
||||
class="tw-rounded-2xl tw-bg-primary-100 tw-border-primary-600 tw-border-solid tw-border tw-p-4 tw-pt-2 tw-flex tw-flex-col tw-gap-2 tw-mb-4"
|
||||
>
|
||||
<div class="tw-flex tw-justify-between tw-items-start tw-flex-grow">
|
||||
<div>
|
||||
@@ -20,6 +20,7 @@
|
||||
(click)="handleDismiss()"
|
||||
[attr.title]="'close' | i18n"
|
||||
[attr.aria-label]="'close' | i18n"
|
||||
class="-tw-me-2"
|
||||
></button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,12 +3,10 @@ import { Observable, combineLatest, from, of } from "rxjs";
|
||||
import { catchError, switchMap } from "rxjs/operators";
|
||||
|
||||
import { VaultProfileService } from "@bitwarden/angular/vault/services/vault-profile.service";
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { BiometricStateService } from "@bitwarden/key-management";
|
||||
|
||||
@@ -5,7 +5,7 @@ import { combineLatest, Observable, of, switchMap } from "rxjs";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
|
||||
import { DefaultSingleNudgeService } from "../default-single-nudge.service";
|
||||
@@ -27,7 +27,7 @@ export class EmptyVaultNudgeService extends DefaultSingleNudgeService {
|
||||
this.getNudgeStatus$(nudgeType, userId),
|
||||
this.cipherService.cipherListViews$(userId),
|
||||
this.organizationService.organizations$(userId),
|
||||
this.collectionService.decryptedCollections$,
|
||||
this.collectionService.decryptedCollections$(userId),
|
||||
]).pipe(
|
||||
switchMap(([nudgeStatus, ciphers, orgs, collections]) => {
|
||||
const vaultHasContents = !(ciphers == null || ciphers.length === 0);
|
||||
@@ -42,7 +42,7 @@ export class EmptyVaultNudgeService extends DefaultSingleNudgeService {
|
||||
const orgIds = new Set(orgs.map((org) => org.id));
|
||||
const canCreateCollections = orgs.some((org) => org.canCreateNewCollections);
|
||||
const hasManageCollections = collections.some(
|
||||
(c) => c.manage && orgIds.has(c.organizationId!),
|
||||
(c) => c.manage && orgIds.has(c.organizationId! as OrganizationId),
|
||||
);
|
||||
|
||||
// When the user has dismissed the nudge or spotlight, return the nudge status directly
|
||||
|
||||
@@ -5,7 +5,7 @@ import { combineLatest, Observable, of, switchMap } from "rxjs";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
|
||||
import { DefaultSingleNudgeService } from "../default-single-nudge.service";
|
||||
@@ -27,7 +27,7 @@ export class VaultSettingsImportNudgeService extends DefaultSingleNudgeService {
|
||||
this.getNudgeStatus$(nudgeType, userId),
|
||||
this.cipherService.cipherViews$(userId),
|
||||
this.organizationService.organizations$(userId),
|
||||
this.collectionService.decryptedCollections$,
|
||||
this.collectionService.decryptedCollections$(userId),
|
||||
]).pipe(
|
||||
switchMap(([nudgeStatus, ciphers, orgs, collections]) => {
|
||||
const vaultHasMoreThanOneItem = (ciphers?.length ?? 0) > 1;
|
||||
@@ -46,7 +46,7 @@ export class VaultSettingsImportNudgeService extends DefaultSingleNudgeService {
|
||||
const orgIds = new Set(orgs.map((org) => org.id));
|
||||
const canCreateCollections = orgs.some((org) => org.canCreateNewCollections);
|
||||
const hasManageCollections = collections.some(
|
||||
(c) => c.manage && orgIds.has(c.organizationId!),
|
||||
(c) => c.manage && orgIds.has(c.organizationId! as OrganizationId),
|
||||
);
|
||||
|
||||
// When the user has dismissed the nudge or spotlight, return the nudge status directly
|
||||
|
||||
@@ -2,13 +2,11 @@ import { TestBed } from "@angular/core/testing";
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { firstValueFrom, of } from "rxjs";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
|
||||
@@ -109,7 +109,12 @@ export class VaultFilterService implements DeprecatedVaultFilterServiceAbstracti
|
||||
}
|
||||
|
||||
async buildCollections(organizationId?: string): Promise<DynamicTreeNode<CollectionView>> {
|
||||
const storedCollections = await this.collectionService.getAllDecrypted();
|
||||
const storedCollections = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.collectionService.decryptedCollections$(userId)),
|
||||
),
|
||||
);
|
||||
const orgs = await this.buildOrganizations();
|
||||
const defaulCollectionsFlagEnabled = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.CreateDefaultLocation,
|
||||
|
||||
Reference in New Issue
Block a user