mirror of
https://github.com/bitwarden/browser
synced 2025-12-12 22:33:35 +00:00
chore(feature flags): [PM-19034] Remove feature flags and old components for Set/Change Password
* Removed flag and components. * More cleanup * Removed ChangePasswordComponent. * Removed old EmergencyAccessTakeover * Removed service initialization. * Fixed test failures. * Fixed tests. * Test changes. * Updated comments * Fixed tests. * Fixed tests. * Fixed merge conflict. * Removed style and routing references. * Better comments. * Removed ResetPasswordComponent
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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,13 +22,11 @@ import {
|
||||
DefaultLoginComponentService,
|
||||
DefaultLoginDecryptionOptionsService,
|
||||
DefaultRegistrationFinishService,
|
||||
DefaultSetPasswordJitService,
|
||||
DefaultTwoFactorAuthComponentService,
|
||||
DefaultTwoFactorAuthWebAuthnComponentService,
|
||||
LoginComponentService,
|
||||
LoginDecryptionOptionsService,
|
||||
RegistrationFinishService as RegistrationFinishServiceAbstraction,
|
||||
SetPasswordJitService,
|
||||
TwoFactorAuthComponentService,
|
||||
TwoFactorAuthWebAuthnComponentService,
|
||||
} from "@bitwarden/auth/angular";
|
||||
@@ -1417,21 +1415,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,
|
||||
|
||||
Reference in New Issue
Block a user