mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 05:43:41 +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,
|
||||
|
||||
@@ -41,11 +41,6 @@ export * from "./registration/registration-env-selector/registration-env-selecto
|
||||
export * from "./registration/registration-finish/registration-finish.service";
|
||||
export * from "./registration/registration-finish/default-registration-finish.service";
|
||||
|
||||
// set password (JIT user)
|
||||
export * from "./set-password-jit/set-password-jit.component";
|
||||
export * from "./set-password-jit/set-password-jit.service.abstraction";
|
||||
export * from "./set-password-jit/default-set-password-jit.service";
|
||||
|
||||
// user verification
|
||||
export * from "./user-verification/user-verification-dialog.component";
|
||||
export * from "./user-verification/user-verification-dialog.types";
|
||||
|
||||
@@ -18,7 +18,6 @@ import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
|
||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { ClientType, HttpStatusCode } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
@@ -230,29 +229,21 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
|
||||
let credentials: PasswordLoginCredentials;
|
||||
// Try to retrieve any org policies from an org invite now so we can send it to the
|
||||
// login strategies. Since it is optional and we only want to be doing this on the
|
||||
// web we will only send in content in the right context.
|
||||
const orgPoliciesFromInvite = this.loginComponentService.getOrgPoliciesFromOrgInvite
|
||||
? await this.loginComponentService.getOrgPoliciesFromOrgInvite()
|
||||
: null;
|
||||
|
||||
if (
|
||||
await this.configService.getFeatureFlag(FeatureFlag.PM16117_ChangeExistingPasswordRefactor)
|
||||
) {
|
||||
// Try to retrieve any org policies from an org invite now so we can send it to the
|
||||
// login strategies. Since it is optional and we only want to be doing this on the
|
||||
// web we will only send in content in the right context.
|
||||
const orgPoliciesFromInvite = this.loginComponentService.getOrgPoliciesFromOrgInvite
|
||||
? await this.loginComponentService.getOrgPoliciesFromOrgInvite()
|
||||
: null;
|
||||
const orgMasterPasswordPolicyOptions = orgPoliciesFromInvite?.enforcedPasswordPolicyOptions;
|
||||
|
||||
const orgMasterPasswordPolicyOptions = orgPoliciesFromInvite?.enforcedPasswordPolicyOptions;
|
||||
|
||||
credentials = new PasswordLoginCredentials(
|
||||
email,
|
||||
masterPassword,
|
||||
undefined,
|
||||
orgMasterPasswordPolicyOptions,
|
||||
);
|
||||
} else {
|
||||
credentials = new PasswordLoginCredentials(email, masterPassword);
|
||||
}
|
||||
const credentials = new PasswordLoginCredentials(
|
||||
email,
|
||||
masterPassword,
|
||||
undefined,
|
||||
orgMasterPasswordPolicyOptions,
|
||||
);
|
||||
|
||||
try {
|
||||
const authResult = await this.loginStrategyService.logIn(credentials);
|
||||
@@ -332,7 +323,7 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||
await this.loginSuccessHandlerService.run(authResult.userId);
|
||||
|
||||
// Determine where to send the user next
|
||||
// The AuthGuard will handle routing to update-temp-password based on state
|
||||
// The AuthGuard will handle routing to change-password based on state
|
||||
|
||||
// TODO: PM-18269 - evaluate if we can combine this with the
|
||||
// password evaluation done in the password login strategy.
|
||||
@@ -344,7 +335,7 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||
|
||||
if (orgPolicies) {
|
||||
// Since we have retrieved the policies, we can go ahead and set them into state for future use
|
||||
// e.g., the update-password page currently only references state for policy data and
|
||||
// e.g., the change-password page currently only references state for policy data and
|
||||
// doesn't fallback to pulling them from the server like it should if they are null.
|
||||
await this.setPoliciesIntoState(authResult.userId, orgPolicies.policies);
|
||||
|
||||
@@ -352,13 +343,7 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||
orgPolicies.enforcedPasswordPolicyOptions,
|
||||
);
|
||||
if (isPasswordChangeRequired) {
|
||||
const changePasswordFeatureFlagOn = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.PM16117_ChangeExistingPasswordRefactor,
|
||||
);
|
||||
|
||||
await this.router.navigate(
|
||||
changePasswordFeatureFlagOn ? ["change-password"] : ["update-password"],
|
||||
);
|
||||
await this.router.navigate(["change-password"]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -151,25 +150,17 @@ export class NewDeviceVerificationComponent implements OnInit, OnDestroy {
|
||||
this.loginSuccessHandlerService.run(authResult.userId);
|
||||
|
||||
// TODO: PM-22663 use the new service to handle routing.
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
|
||||
const forceSetPasswordReason = await firstValueFrom(
|
||||
this.masterPasswordService.forceSetPasswordReason$(activeUserId),
|
||||
);
|
||||
|
||||
if (
|
||||
await this.configService.getFeatureFlag(FeatureFlag.PM16117_ChangeExistingPasswordRefactor)
|
||||
forceSetPasswordReason === ForceSetPasswordReason.WeakMasterPassword ||
|
||||
forceSetPasswordReason === ForceSetPasswordReason.AdminForcePasswordReset
|
||||
) {
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(getUserId),
|
||||
);
|
||||
|
||||
const forceSetPasswordReason = await firstValueFrom(
|
||||
this.masterPasswordService.forceSetPasswordReason$(activeUserId),
|
||||
);
|
||||
|
||||
if (
|
||||
forceSetPasswordReason === ForceSetPasswordReason.WeakMasterPassword ||
|
||||
forceSetPasswordReason === ForceSetPasswordReason.AdminForcePasswordReset
|
||||
) {
|
||||
await this.router.navigate(["/change-password"]);
|
||||
} else {
|
||||
await this.router.navigate(["/vault"]);
|
||||
}
|
||||
await this.router.navigate(["/change-password"]);
|
||||
} else {
|
||||
await this.router.navigate(["/vault"]);
|
||||
}
|
||||
|
||||
@@ -1,241 +0,0 @@
|
||||
import { MockProxy, mock } from "jest-mock-extended";
|
||||
import { BehaviorSubject, 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 { OrganizationUserApiService } from "@bitwarden/admin-console/common";
|
||||
import {
|
||||
FakeUserDecryptionOptions as UserDecryptionOptions,
|
||||
InternalUserDecryptionOptionsServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { OrganizationKeysResponse } from "@bitwarden/common/admin-console/models/response/organization-keys.response";
|
||||
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
|
||||
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 { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { CsprngArray } from "@bitwarden/common/types/csprng";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
|
||||
import { DEFAULT_KDF_CONFIG, KdfConfigService, KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { PasswordInputResult } from "../input-password/password-input-result";
|
||||
|
||||
import { DefaultSetPasswordJitService } from "./default-set-password-jit.service";
|
||||
import { SetPasswordCredentials } from "./set-password-jit.service.abstraction";
|
||||
|
||||
describe("DefaultSetPasswordJitService", () => {
|
||||
let sut: DefaultSetPasswordJitService;
|
||||
|
||||
let masterPasswordApiService: MockProxy<MasterPasswordApiService>;
|
||||
let keyService: MockProxy<KeyService>;
|
||||
let encryptService: MockProxy<EncryptService>;
|
||||
let i18nService: MockProxy<I18nService>;
|
||||
let kdfConfigService: MockProxy<KdfConfigService>;
|
||||
let masterPasswordService: MockProxy<InternalMasterPasswordServiceAbstraction>;
|
||||
let organizationApiService: MockProxy<OrganizationApiServiceAbstraction>;
|
||||
let organizationUserApiService: MockProxy<OrganizationUserApiService>;
|
||||
let userDecryptionOptionsService: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>;
|
||||
|
||||
beforeEach(() => {
|
||||
masterPasswordApiService = mock<MasterPasswordApiService>();
|
||||
keyService = mock<KeyService>();
|
||||
encryptService = mock<EncryptService>();
|
||||
i18nService = mock<I18nService>();
|
||||
kdfConfigService = mock<KdfConfigService>();
|
||||
masterPasswordService = mock<InternalMasterPasswordServiceAbstraction>();
|
||||
organizationApiService = mock<OrganizationApiServiceAbstraction>();
|
||||
organizationUserApiService = mock<OrganizationUserApiService>();
|
||||
userDecryptionOptionsService = mock<InternalUserDecryptionOptionsServiceAbstraction>();
|
||||
|
||||
sut = new DefaultSetPasswordJitService(
|
||||
encryptService,
|
||||
i18nService,
|
||||
kdfConfigService,
|
||||
keyService,
|
||||
masterPasswordApiService,
|
||||
masterPasswordService,
|
||||
organizationApiService,
|
||||
organizationUserApiService,
|
||||
userDecryptionOptionsService,
|
||||
);
|
||||
});
|
||||
|
||||
it("should instantiate the DefaultSetPasswordJitService", () => {
|
||||
expect(sut).not.toBeFalsy();
|
||||
});
|
||||
|
||||
describe("setPassword", () => {
|
||||
let masterKey: MasterKey;
|
||||
let userKey: UserKey;
|
||||
let userKeyEncString: EncString;
|
||||
let protectedUserKey: [UserKey, EncString];
|
||||
let keyPair: [string, EncString];
|
||||
let keysRequest: KeysRequest;
|
||||
let organizationKeys: OrganizationKeysResponse;
|
||||
let orgPublicKey: Uint8Array;
|
||||
|
||||
let orgSsoIdentifier: string;
|
||||
let orgId: string;
|
||||
let resetPasswordAutoEnroll: boolean;
|
||||
let userId: UserId;
|
||||
let passwordInputResult: PasswordInputResult;
|
||||
let credentials: SetPasswordCredentials;
|
||||
|
||||
let userDecryptionOptionsSubject: BehaviorSubject<UserDecryptionOptions>;
|
||||
let setPasswordRequest: SetPasswordRequest;
|
||||
|
||||
beforeEach(() => {
|
||||
masterKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as MasterKey;
|
||||
userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey;
|
||||
userKeyEncString = new EncString("userKeyEncrypted");
|
||||
protectedUserKey = [userKey, userKeyEncString];
|
||||
keyPair = ["publicKey", new EncString("privateKey")];
|
||||
keysRequest = new KeysRequest(keyPair[0], keyPair[1].encryptedString);
|
||||
organizationKeys = {
|
||||
privateKey: "orgPrivateKey",
|
||||
publicKey: "orgPublicKey",
|
||||
} as OrganizationKeysResponse;
|
||||
orgPublicKey = Utils.fromB64ToArray(organizationKeys.publicKey);
|
||||
|
||||
orgSsoIdentifier = "orgSsoIdentifier";
|
||||
orgId = "orgId";
|
||||
resetPasswordAutoEnroll = false;
|
||||
userId = "userId" as UserId;
|
||||
|
||||
passwordInputResult = {
|
||||
newMasterKey: masterKey,
|
||||
newServerMasterKeyHash: "newServerMasterKeyHash",
|
||||
newLocalMasterKeyHash: "newLocalMasterKeyHash",
|
||||
newPasswordHint: "newPasswordHint",
|
||||
kdfConfig: DEFAULT_KDF_CONFIG,
|
||||
newPassword: "newPassword",
|
||||
};
|
||||
|
||||
credentials = {
|
||||
newMasterKey: passwordInputResult.newMasterKey,
|
||||
newServerMasterKeyHash: passwordInputResult.newServerMasterKeyHash,
|
||||
newLocalMasterKeyHash: passwordInputResult.newLocalMasterKeyHash,
|
||||
newPasswordHint: passwordInputResult.newPasswordHint,
|
||||
kdfConfig: passwordInputResult.kdfConfig,
|
||||
orgSsoIdentifier,
|
||||
orgId,
|
||||
resetPasswordAutoEnroll,
|
||||
userId,
|
||||
};
|
||||
|
||||
userDecryptionOptionsSubject = new BehaviorSubject(null);
|
||||
userDecryptionOptionsService.userDecryptionOptions$ = userDecryptionOptionsSubject;
|
||||
|
||||
setPasswordRequest = new SetPasswordRequest(
|
||||
passwordInputResult.newServerMasterKeyHash,
|
||||
protectedUserKey[1].encryptedString,
|
||||
passwordInputResult.newPasswordHint,
|
||||
orgSsoIdentifier,
|
||||
keysRequest,
|
||||
passwordInputResult.kdfConfig.kdfType,
|
||||
passwordInputResult.kdfConfig.iterations,
|
||||
);
|
||||
});
|
||||
|
||||
function setupSetPasswordMocks(hasUserKey = true) {
|
||||
if (!hasUserKey) {
|
||||
keyService.userKey$.mockReturnValue(of(null));
|
||||
keyService.makeUserKey.mockResolvedValue(protectedUserKey);
|
||||
} else {
|
||||
keyService.userKey$.mockReturnValue(of(userKey));
|
||||
keyService.encryptUserKeyWithMasterKey.mockResolvedValue(protectedUserKey);
|
||||
}
|
||||
|
||||
keyService.makeKeyPair.mockResolvedValue(keyPair);
|
||||
|
||||
masterPasswordApiService.setPassword.mockResolvedValue(undefined);
|
||||
masterPasswordService.setForceSetPasswordReason.mockResolvedValue(undefined);
|
||||
|
||||
userDecryptionOptionsSubject.next(new UserDecryptionOptions({ hasMasterPassword: true }));
|
||||
userDecryptionOptionsService.setUserDecryptionOptions.mockResolvedValue(undefined);
|
||||
kdfConfigService.setKdfConfig.mockResolvedValue(undefined);
|
||||
keyService.setUserKey.mockResolvedValue(undefined);
|
||||
|
||||
keyService.setPrivateKey.mockResolvedValue(undefined);
|
||||
|
||||
masterPasswordService.setMasterKeyHash.mockResolvedValue(undefined);
|
||||
}
|
||||
|
||||
function setupResetPasswordAutoEnrollMocks(organizationKeysExist = true) {
|
||||
if (organizationKeysExist) {
|
||||
organizationApiService.getKeys.mockResolvedValue(organizationKeys);
|
||||
} else {
|
||||
organizationApiService.getKeys.mockResolvedValue(null);
|
||||
return;
|
||||
}
|
||||
|
||||
keyService.userKey$.mockReturnValue(of(userKey));
|
||||
encryptService.encapsulateKeyUnsigned.mockResolvedValue(userKeyEncString);
|
||||
|
||||
organizationUserApiService.putOrganizationUserResetPasswordEnrollment.mockResolvedValue(
|
||||
undefined,
|
||||
);
|
||||
}
|
||||
|
||||
it("should set password successfully (given a user key)", async () => {
|
||||
// Arrange
|
||||
setupSetPasswordMocks();
|
||||
|
||||
// Act
|
||||
await sut.setPassword(credentials);
|
||||
|
||||
// Assert
|
||||
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
|
||||
});
|
||||
|
||||
it("should set password successfully (given no user key)", async () => {
|
||||
// Arrange
|
||||
setupSetPasswordMocks(false);
|
||||
|
||||
// Act
|
||||
await sut.setPassword(credentials);
|
||||
|
||||
// Assert
|
||||
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
|
||||
});
|
||||
|
||||
it("should handle reset password auto enroll", async () => {
|
||||
// Arrange
|
||||
credentials.resetPasswordAutoEnroll = true;
|
||||
|
||||
setupSetPasswordMocks();
|
||||
setupResetPasswordAutoEnrollMocks();
|
||||
|
||||
// Act
|
||||
await sut.setPassword(credentials);
|
||||
|
||||
// Assert
|
||||
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
|
||||
expect(organizationApiService.getKeys).toHaveBeenCalledWith(orgId);
|
||||
expect(encryptService.encapsulateKeyUnsigned).toHaveBeenCalledWith(userKey, orgPublicKey);
|
||||
expect(
|
||||
organizationUserApiService.putOrganizationUserResetPasswordEnrollment,
|
||||
).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("when handling reset password auto enroll, it should throw an error if organization keys are not found", async () => {
|
||||
// Arrange
|
||||
credentials.resetPasswordAutoEnroll = true;
|
||||
|
||||
setupSetPasswordMocks();
|
||||
setupResetPasswordAutoEnrollMocks(false);
|
||||
|
||||
// Act and Assert
|
||||
await expect(sut.setPassword(credentials)).rejects.toThrow();
|
||||
expect(
|
||||
organizationUserApiService.putOrganizationUserResetPasswordEnrollment,
|
||||
).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,176 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
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 {
|
||||
OrganizationUserApiService,
|
||||
OrganizationUserResetPasswordEnrollmentRequest,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.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 { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
|
||||
import { KdfConfigService, KeyService, KdfConfig } from "@bitwarden/key-management";
|
||||
|
||||
import {
|
||||
SetPasswordCredentials,
|
||||
SetPasswordJitService,
|
||||
} from "./set-password-jit.service.abstraction";
|
||||
|
||||
export class DefaultSetPasswordJitService implements SetPasswordJitService {
|
||||
constructor(
|
||||
protected encryptService: EncryptService,
|
||||
protected i18nService: I18nService,
|
||||
protected kdfConfigService: KdfConfigService,
|
||||
protected keyService: KeyService,
|
||||
protected masterPasswordApiService: MasterPasswordApiService,
|
||||
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
protected organizationApiService: OrganizationApiServiceAbstraction,
|
||||
protected organizationUserApiService: OrganizationUserApiService,
|
||||
protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
|
||||
) {}
|
||||
|
||||
async setPassword(credentials: SetPasswordCredentials): Promise<void> {
|
||||
const {
|
||||
newMasterKey,
|
||||
newServerMasterKeyHash,
|
||||
newLocalMasterKeyHash,
|
||||
newPasswordHint,
|
||||
kdfConfig,
|
||||
orgSsoIdentifier,
|
||||
orgId,
|
||||
resetPasswordAutoEnroll,
|
||||
userId,
|
||||
} = credentials;
|
||||
|
||||
for (const [key, value] of Object.entries(credentials)) {
|
||||
if (value == null) {
|
||||
throw new Error(`${key} not found. Could not set password.`);
|
||||
}
|
||||
}
|
||||
|
||||
const protectedUserKey = await this.makeProtectedUserKey(newMasterKey, userId);
|
||||
if (protectedUserKey == null) {
|
||||
throw new Error("protectedUserKey not found. Could not set password.");
|
||||
}
|
||||
|
||||
// Since this is an existing JIT provisioned user in a MP encryption org setting first password,
|
||||
// they will not already have a user asymmetric key pair so we must create it for them.
|
||||
const [keyPair, keysRequest] = await this.makeKeyPairAndRequest(protectedUserKey);
|
||||
|
||||
const request = new SetPasswordRequest(
|
||||
newServerMasterKeyHash,
|
||||
protectedUserKey[1].encryptedString,
|
||||
newPasswordHint,
|
||||
orgSsoIdentifier,
|
||||
keysRequest,
|
||||
kdfConfig.kdfType,
|
||||
kdfConfig.iterations,
|
||||
);
|
||||
|
||||
await this.masterPasswordApiService.setPassword(request);
|
||||
|
||||
// Clear force set password reason to allow navigation back to vault.
|
||||
await this.masterPasswordService.setForceSetPasswordReason(ForceSetPasswordReason.None, userId);
|
||||
|
||||
// User now has a password so update account decryption options in state
|
||||
await this.updateAccountDecryptionProperties(newMasterKey, kdfConfig, protectedUserKey, userId);
|
||||
|
||||
await this.keyService.setPrivateKey(keyPair[1].encryptedString, userId);
|
||||
|
||||
await this.masterPasswordService.setMasterKeyHash(newLocalMasterKeyHash, userId);
|
||||
|
||||
if (resetPasswordAutoEnroll) {
|
||||
await this.handleResetPasswordAutoEnroll(newServerMasterKeyHash, orgId, userId);
|
||||
}
|
||||
}
|
||||
|
||||
private async makeProtectedUserKey(
|
||||
masterKey: MasterKey,
|
||||
userId: UserId,
|
||||
): Promise<[UserKey, EncString]> {
|
||||
let protectedUserKey: [UserKey, EncString] = null;
|
||||
|
||||
const userKey = await firstValueFrom(this.keyService.userKey$(userId));
|
||||
|
||||
if (userKey == null) {
|
||||
protectedUserKey = await this.keyService.makeUserKey(masterKey);
|
||||
} else {
|
||||
protectedUserKey = await this.keyService.encryptUserKeyWithMasterKey(masterKey);
|
||||
}
|
||||
|
||||
return protectedUserKey;
|
||||
}
|
||||
|
||||
private async makeKeyPairAndRequest(
|
||||
protectedUserKey: [UserKey, EncString],
|
||||
): Promise<[[string, EncString], KeysRequest]> {
|
||||
const keyPair = await this.keyService.makeKeyPair(protectedUserKey[0]);
|
||||
if (keyPair == null) {
|
||||
throw new Error("keyPair not found. Could not set password.");
|
||||
}
|
||||
const keysRequest = new KeysRequest(keyPair[0], keyPair[1].encryptedString);
|
||||
|
||||
return [keyPair, keysRequest];
|
||||
}
|
||||
|
||||
private async updateAccountDecryptionProperties(
|
||||
masterKey: MasterKey,
|
||||
kdfConfig: KdfConfig,
|
||||
protectedUserKey: [UserKey, EncString],
|
||||
userId: UserId,
|
||||
) {
|
||||
const userDecryptionOpts = await firstValueFrom(
|
||||
this.userDecryptionOptionsService.userDecryptionOptions$,
|
||||
);
|
||||
userDecryptionOpts.hasMasterPassword = true;
|
||||
await this.userDecryptionOptionsService.setUserDecryptionOptions(userDecryptionOpts);
|
||||
await this.kdfConfigService.setKdfConfig(userId, kdfConfig);
|
||||
await this.masterPasswordService.setMasterKey(masterKey, userId);
|
||||
await this.keyService.setUserKey(protectedUserKey[0], userId);
|
||||
}
|
||||
|
||||
private async handleResetPasswordAutoEnroll(
|
||||
masterKeyHash: string,
|
||||
orgId: string,
|
||||
userId: UserId,
|
||||
) {
|
||||
const organizationKeys = await this.organizationApiService.getKeys(orgId);
|
||||
|
||||
if (organizationKeys == null) {
|
||||
throw new Error(this.i18nService.t("resetPasswordOrgKeysError"));
|
||||
}
|
||||
|
||||
const publicKey = Utils.fromB64ToArray(organizationKeys.publicKey);
|
||||
|
||||
// RSA Encrypt user key with organization public key
|
||||
const userKey = await firstValueFrom(this.keyService.userKey$(userId));
|
||||
|
||||
if (userKey == null) {
|
||||
throw new Error("userKey not found. Could not handle reset password auto enroll.");
|
||||
}
|
||||
|
||||
const encryptedUserKey = await this.encryptService.encapsulateKeyUnsigned(userKey, publicKey);
|
||||
|
||||
const resetRequest = new OrganizationUserResetPasswordEnrollmentRequest();
|
||||
resetRequest.masterPasswordHash = masterKeyHash;
|
||||
resetRequest.resetPasswordKey = encryptedUserKey.encryptedString;
|
||||
|
||||
await this.organizationUserApiService.putOrganizationUserResetPasswordEnrollment(
|
||||
orgId,
|
||||
userId,
|
||||
resetRequest,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
<ng-container *ngIf="syncLoading">
|
||||
<i class="bwi bwi-spinner bwi-spin tw-mr-2" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
{{ "loading" | i18n }}
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="!syncLoading">
|
||||
<app-callout
|
||||
type="warning"
|
||||
title="{{ 'resetPasswordPolicyAutoEnroll' | i18n }}"
|
||||
*ngIf="resetPasswordAutoEnroll"
|
||||
>
|
||||
{{ "resetPasswordAutoEnrollInviteWarning" | i18n }}
|
||||
</app-callout>
|
||||
|
||||
<auth-input-password
|
||||
[flow]="inputPasswordFlow"
|
||||
[email]="email"
|
||||
[userId]="userId"
|
||||
[loading]="submitting"
|
||||
[masterPasswordPolicyOptions]="masterPasswordPolicyOptions"
|
||||
[primaryButtonText]="{ key: 'createAccount' }"
|
||||
(onPasswordFormSubmit)="handlePasswordFormSubmit($event)"
|
||||
></auth-input-password>
|
||||
</ng-container>
|
||||
@@ -1,135 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
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 { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
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 { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
|
||||
// FIXME: remove `src` and fix import
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { ToastService } from "../../../../components/src/toast";
|
||||
import {
|
||||
InputPasswordComponent,
|
||||
InputPasswordFlow,
|
||||
} from "../input-password/input-password.component";
|
||||
import { PasswordInputResult } from "../input-password/password-input-result";
|
||||
|
||||
import {
|
||||
SetPasswordCredentials,
|
||||
SetPasswordJitService,
|
||||
} from "./set-password-jit.service.abstraction";
|
||||
|
||||
@Component({
|
||||
selector: "auth-set-password-jit",
|
||||
templateUrl: "set-password-jit.component.html",
|
||||
imports: [CommonModule, InputPasswordComponent, JslibModule],
|
||||
})
|
||||
export class SetPasswordJitComponent implements OnInit {
|
||||
protected inputPasswordFlow = InputPasswordFlow.SetInitialPasswordAuthedUser;
|
||||
protected email: string;
|
||||
protected masterPasswordPolicyOptions: MasterPasswordPolicyOptions;
|
||||
protected orgId: string;
|
||||
protected orgSsoIdentifier: string;
|
||||
protected resetPasswordAutoEnroll: boolean;
|
||||
protected submitting = false;
|
||||
protected syncLoading = true;
|
||||
protected userId: UserId;
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private i18nService: I18nService,
|
||||
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||
private policyApiService: PolicyApiServiceAbstraction,
|
||||
private router: Router,
|
||||
private setPasswordJitService: SetPasswordJitService,
|
||||
private syncService: SyncService,
|
||||
private toastService: ToastService,
|
||||
private validationService: ValidationService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||
this.userId = activeAccount?.id;
|
||||
this.email = activeAccount?.email;
|
||||
|
||||
await this.syncService.fullSync(true);
|
||||
this.syncLoading = false;
|
||||
|
||||
await this.handleQueryParams();
|
||||
}
|
||||
|
||||
private async handleQueryParams() {
|
||||
const qParams = await firstValueFrom(this.activatedRoute.queryParams);
|
||||
|
||||
if (qParams.identifier != null) {
|
||||
try {
|
||||
this.orgSsoIdentifier = qParams.identifier;
|
||||
|
||||
const autoEnrollStatus = await this.organizationApiService.getAutoEnrollStatus(
|
||||
this.orgSsoIdentifier,
|
||||
);
|
||||
this.orgId = autoEnrollStatus.id;
|
||||
this.resetPasswordAutoEnroll = autoEnrollStatus.resetPasswordEnabled;
|
||||
this.masterPasswordPolicyOptions =
|
||||
await this.policyApiService.getMasterPasswordPolicyOptsForOrgUser(autoEnrollStatus.id);
|
||||
} catch {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: null,
|
||||
message: this.i18nService.t("errorOccurred"),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected async handlePasswordFormSubmit(passwordInputResult: PasswordInputResult) {
|
||||
this.submitting = true;
|
||||
|
||||
const credentials: SetPasswordCredentials = {
|
||||
newMasterKey: passwordInputResult.newMasterKey,
|
||||
newServerMasterKeyHash: passwordInputResult.newServerMasterKeyHash,
|
||||
newLocalMasterKeyHash: passwordInputResult.newLocalMasterKeyHash,
|
||||
newPasswordHint: passwordInputResult.newPasswordHint,
|
||||
kdfConfig: passwordInputResult.kdfConfig,
|
||||
orgSsoIdentifier: this.orgSsoIdentifier,
|
||||
orgId: this.orgId,
|
||||
resetPasswordAutoEnroll: this.resetPasswordAutoEnroll,
|
||||
userId: this.userId,
|
||||
};
|
||||
|
||||
try {
|
||||
await this.setPasswordJitService.setPassword(credentials);
|
||||
} catch (e) {
|
||||
this.validationService.showError(e);
|
||||
this.submitting = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("accountSuccessfullyCreated"),
|
||||
});
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("inviteAccepted"),
|
||||
});
|
||||
|
||||
this.submitting = false;
|
||||
|
||||
await this.router.navigate(["vault"]);
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { MasterKey } from "@bitwarden/common/types/key";
|
||||
import { KdfConfig } from "@bitwarden/key-management";
|
||||
|
||||
export interface SetPasswordCredentials {
|
||||
newMasterKey: MasterKey;
|
||||
newServerMasterKeyHash: string;
|
||||
newLocalMasterKeyHash: string;
|
||||
newPasswordHint: string;
|
||||
kdfConfig: KdfConfig;
|
||||
orgSsoIdentifier: string;
|
||||
orgId: string;
|
||||
resetPasswordAutoEnroll: boolean;
|
||||
userId: UserId;
|
||||
}
|
||||
|
||||
/**
|
||||
* This service handles setting a password for a "just-in-time" provisioned user.
|
||||
*
|
||||
* A "just-in-time" (JIT) provisioned user is a user who does not have a registered account at the
|
||||
* time they first click "Login with SSO". Once they click "Login with SSO" we register the account on
|
||||
* the fly ("just-in-time").
|
||||
*/
|
||||
export abstract class SetPasswordJitService {
|
||||
/**
|
||||
* Sets the password for a JIT provisioned user.
|
||||
*
|
||||
* @param credentials An object of the credentials needed to set the password for a JIT provisioned user
|
||||
* @throws If any property on the `credentials` object is null or undefined, or if a protectedUserKey
|
||||
* or newKeyPair could not be created.
|
||||
*/
|
||||
abstract setPassword(credentials: SetPasswordCredentials): Promise<void>;
|
||||
}
|
||||
@@ -23,12 +23,10 @@ import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { SsoPreValidateResponse } from "@bitwarden/common/auth/models/response/sso-pre-validate.response";
|
||||
import { ClientType, HttpStatusCode } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
@@ -118,7 +116,6 @@ export class SsoComponent implements OnInit {
|
||||
private toastService: ToastService,
|
||||
private ssoComponentService: SsoComponentService,
|
||||
private loginSuccessHandlerService: LoginSuccessHandlerService,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
environmentService.environment$.pipe(takeUntilDestroyed()).subscribe((env) => {
|
||||
this.redirectUri = env.getWebVaultUrl() + "/sso-connector.html";
|
||||
@@ -534,11 +531,7 @@ export class SsoComponent implements OnInit {
|
||||
}
|
||||
|
||||
private async handleChangePasswordRequired(orgIdentifier: string) {
|
||||
const isSetInitialPasswordRefactorFlagOn = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.PM16117_SetInitialPasswordRefactor,
|
||||
);
|
||||
const route = isSetInitialPasswordRefactorFlagOn ? "set-initial-password" : "set-password-jit";
|
||||
|
||||
const route = "set-initial-password";
|
||||
await this.router.navigate([route], {
|
||||
queryParams: {
|
||||
identifier: orgIdentifier,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Component } from "@angular/core";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { ActivatedRoute, Router, convertToParamMap } from "@angular/router";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
|
||||
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
|
||||
import {
|
||||
@@ -24,8 +24,10 @@ import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-
|
||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { FakeMasterPasswordService } from "@bitwarden/common/key-management/master-password/services/fake-master-password.service";
|
||||
import {
|
||||
InternalMasterPasswordServiceAbstraction,
|
||||
MasterPasswordServiceAbstraction,
|
||||
} from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
@@ -66,7 +68,7 @@ describe("TwoFactorAuthComponent", () => {
|
||||
let mockLoginEmailService: MockProxy<LoginEmailServiceAbstraction>;
|
||||
let mockUserDecryptionOptionsService: MockProxy<UserDecryptionOptionsServiceAbstraction>;
|
||||
let mockSsoLoginService: MockProxy<SsoLoginServiceAbstraction>;
|
||||
let mockMasterPasswordService: FakeMasterPasswordService;
|
||||
let mockMasterPasswordService: MockProxy<InternalMasterPasswordServiceAbstraction>;
|
||||
let mockAccountService: FakeAccountService;
|
||||
let mockDialogService: MockProxy<DialogService>;
|
||||
let mockToastService: MockProxy<ToastService>;
|
||||
@@ -107,7 +109,7 @@ describe("TwoFactorAuthComponent", () => {
|
||||
mockUserDecryptionOptionsService = mock<UserDecryptionOptionsServiceAbstraction>();
|
||||
mockSsoLoginService = mock<SsoLoginServiceAbstraction>();
|
||||
mockAccountService = mockAccountServiceWith(userId);
|
||||
mockMasterPasswordService = new FakeMasterPasswordService();
|
||||
mockMasterPasswordService = mock<InternalMasterPasswordServiceAbstraction>();
|
||||
mockDialogService = mock<DialogService>();
|
||||
mockToastService = mock<ToastService>();
|
||||
mockTwoFactorAuthCompService = mock<TwoFactorAuthComponentService>();
|
||||
@@ -212,6 +214,7 @@ describe("TwoFactorAuthComponent", () => {
|
||||
},
|
||||
{ provide: AuthService, useValue: mockAuthService },
|
||||
{ provide: ConfigService, useValue: mockConfigService },
|
||||
{ provide: MasterPasswordServiceAbstraction, useValue: mockMasterPasswordService },
|
||||
],
|
||||
});
|
||||
|
||||
@@ -267,54 +270,16 @@ describe("TwoFactorAuthComponent", () => {
|
||||
selectedUserDecryptionOptions.next(mockUserDecryptionOpts.noMasterPassword);
|
||||
});
|
||||
|
||||
describe("Given the PM16117_SetInitialPasswordRefactor feature flag is ON", () => {
|
||||
it("navigates to the /set-initial-password route when user doesn't have a MP and key connector isn't enabled", async () => {
|
||||
// Arrange
|
||||
mockConfigService.getFeatureFlag.mockResolvedValue(true);
|
||||
|
||||
// Act
|
||||
await component.submit("testToken");
|
||||
|
||||
// Assert
|
||||
expect(mockRouter.navigate).toHaveBeenCalledTimes(1);
|
||||
expect(mockRouter.navigate).toHaveBeenCalledWith(["set-initial-password"], {
|
||||
queryParams: {
|
||||
identifier: component.orgSsoIdentifier,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Given the PM16117_SetInitialPasswordRefactor feature flag is OFF", () => {
|
||||
it("navigates to the /set-password route when user doesn't have a MP and key connector isn't enabled", async () => {
|
||||
// Arrange
|
||||
mockConfigService.getFeatureFlag.mockResolvedValue(false);
|
||||
|
||||
// Act
|
||||
await component.submit("testToken");
|
||||
|
||||
// Assert
|
||||
expect(mockRouter.navigate).toHaveBeenCalledTimes(1);
|
||||
expect(mockRouter.navigate).toHaveBeenCalledWith(["set-password"], {
|
||||
queryParams: {
|
||||
identifier: component.orgSsoIdentifier,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Given the PM16117_SetInitialPasswordRefactor feature flag is ON", () => {
|
||||
it("does not navigate to the /set-initial-password route when the user has key connector even if user has no master password", async () => {
|
||||
it("navigates to the /set-initial-password route when user doesn't have a MP and key connector isn't enabled", async () => {
|
||||
// Arrange
|
||||
mockConfigService.getFeatureFlag.mockResolvedValue(true);
|
||||
|
||||
selectedUserDecryptionOptions.next(
|
||||
mockUserDecryptionOpts.noMasterPasswordWithKeyConnector,
|
||||
);
|
||||
// Act
|
||||
await component.submit("testToken");
|
||||
|
||||
await component.submit(token, remember);
|
||||
|
||||
expect(mockRouter.navigate).not.toHaveBeenCalledWith(["set-initial-password"], {
|
||||
// Assert
|
||||
expect(mockRouter.navigate).toHaveBeenCalledTimes(1);
|
||||
expect(mockRouter.navigate).toHaveBeenCalledWith(["set-initial-password"], {
|
||||
queryParams: {
|
||||
identifier: component.orgSsoIdentifier,
|
||||
},
|
||||
@@ -322,21 +287,19 @@ describe("TwoFactorAuthComponent", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Given the PM16117_SetInitialPasswordRefactor feature flag is OFF", () => {
|
||||
it("does not navigate to the /set-password route when the user has key connector even if user has no master password", async () => {
|
||||
mockConfigService.getFeatureFlag.mockResolvedValue(false);
|
||||
it("does not navigate to the /set-initial-password route when the user has key connector even if user has no master password", async () => {
|
||||
mockConfigService.getFeatureFlag.mockResolvedValue(true);
|
||||
|
||||
selectedUserDecryptionOptions.next(
|
||||
mockUserDecryptionOpts.noMasterPasswordWithKeyConnector,
|
||||
);
|
||||
selectedUserDecryptionOptions.next(
|
||||
mockUserDecryptionOpts.noMasterPasswordWithKeyConnector,
|
||||
);
|
||||
|
||||
await component.submit(token, remember);
|
||||
await component.submit(token, remember);
|
||||
|
||||
expect(mockRouter.navigate).not.toHaveBeenCalledWith(["set-password"], {
|
||||
queryParams: {
|
||||
identifier: component.orgSsoIdentifier,
|
||||
},
|
||||
});
|
||||
expect(mockRouter.navigate).not.toHaveBeenCalledWith(["set-initial-password"], {
|
||||
queryParams: {
|
||||
identifier: component.orgSsoIdentifier,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -344,6 +307,9 @@ describe("TwoFactorAuthComponent", () => {
|
||||
it("navigates to the component's defined success route (vault is default) when the login is successful", async () => {
|
||||
mockLoginStrategyService.logInTwoFactor.mockResolvedValue(new AuthResult());
|
||||
mockAuthService.activeAccountStatus$ = new BehaviorSubject(AuthenticationStatus.Unlocked);
|
||||
mockMasterPasswordService.forceSetPasswordReason$.mockReturnValue(
|
||||
of(ForceSetPasswordReason.None),
|
||||
);
|
||||
|
||||
// Act
|
||||
await component.submit("testToken");
|
||||
@@ -409,7 +375,7 @@ describe("TwoFactorAuthComponent", () => {
|
||||
await component.submit(token, remember);
|
||||
|
||||
// Assert
|
||||
expect(mockMasterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledWith(
|
||||
expect(mockMasterPasswordService.setForceSetPasswordReason).toHaveBeenCalledWith(
|
||||
ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission,
|
||||
userId,
|
||||
);
|
||||
|
||||
@@ -17,7 +17,6 @@ import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
|
||||
import {
|
||||
LoginStrategyServiceAbstraction,
|
||||
LoginEmailServiceAbstraction,
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
TrustedDeviceUserDecryptionOption,
|
||||
UserDecryptionOptions,
|
||||
@@ -32,9 +31,7 @@ import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-p
|
||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
@@ -156,7 +153,6 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy {
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private logService: LogService,
|
||||
private twoFactorService: TwoFactorService,
|
||||
private loginEmailService: LoginEmailServiceAbstraction,
|
||||
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
||||
private ssoLoginService: SsoLoginServiceAbstraction,
|
||||
private masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
@@ -171,7 +167,6 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy {
|
||||
private loginSuccessHandlerService: LoginSuccessHandlerService,
|
||||
private twoFactorAuthComponentCacheService: TwoFactorAuthComponentCacheService,
|
||||
private authService: AuthService,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -507,19 +502,15 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
// TODO: PM-22663 use the new service to handle routing.
|
||||
if (
|
||||
await this.configService.getFeatureFlag(FeatureFlag.PM16117_ChangeExistingPasswordRefactor)
|
||||
) {
|
||||
const forceSetPasswordReason = await firstValueFrom(
|
||||
this.masterPasswordService.forceSetPasswordReason$(userId),
|
||||
);
|
||||
const forceSetPasswordReason = await firstValueFrom(
|
||||
this.masterPasswordService.forceSetPasswordReason$(userId),
|
||||
);
|
||||
|
||||
if (
|
||||
forceSetPasswordReason === ForceSetPasswordReason.WeakMasterPassword ||
|
||||
forceSetPasswordReason === ForceSetPasswordReason.AdminForcePasswordReset
|
||||
) {
|
||||
return "change-password";
|
||||
}
|
||||
if (
|
||||
forceSetPasswordReason === ForceSetPasswordReason.WeakMasterPassword ||
|
||||
forceSetPasswordReason === ForceSetPasswordReason.AdminForcePasswordReset
|
||||
) {
|
||||
return "change-password";
|
||||
}
|
||||
|
||||
return "vault";
|
||||
@@ -575,11 +566,7 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
private async handleChangePasswordRequired(orgIdentifier: string | undefined) {
|
||||
const isSetInitialPasswordRefactorFlagOn = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.PM16117_SetInitialPasswordRefactor,
|
||||
);
|
||||
const route = isSetInitialPasswordRefactorFlagOn ? "set-initial-password" : "set-password";
|
||||
|
||||
const route = "set-initial-password";
|
||||
await this.router.navigate([route], {
|
||||
queryParams: {
|
||||
identifier: orgIdentifier,
|
||||
|
||||
@@ -12,7 +12,6 @@ import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/id
|
||||
import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response";
|
||||
import { MasterPasswordPolicyResponse } from "@bitwarden/common/auth/models/response/master-password-policy.response";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { FakeMasterPasswordService } from "@bitwarden/common/key-management/master-password/services/fake-master-password.service";
|
||||
import {
|
||||
@@ -221,7 +220,10 @@ describe("PasswordLoginStrategy", () => {
|
||||
|
||||
await passwordLoginStrategy.logIn(credentials);
|
||||
|
||||
expect(policyService.evaluateMasterPassword).not.toHaveBeenCalled();
|
||||
expect(masterPasswordService.mock.setForceSetPasswordReason).not.toHaveBeenCalledWith(
|
||||
ForceSetPasswordReason.WeakMasterPassword,
|
||||
userId,
|
||||
);
|
||||
});
|
||||
|
||||
it("does not force the user to update their master password when it meets requirements", async () => {
|
||||
@@ -230,7 +232,10 @@ describe("PasswordLoginStrategy", () => {
|
||||
|
||||
await passwordLoginStrategy.logIn(credentials);
|
||||
|
||||
expect(policyService.evaluateMasterPassword).toHaveBeenCalled();
|
||||
expect(masterPasswordService.mock.setForceSetPasswordReason).not.toHaveBeenCalledWith(
|
||||
ForceSetPasswordReason.WeakMasterPassword,
|
||||
userId,
|
||||
);
|
||||
});
|
||||
|
||||
it("when given master password policies as part of the login credentials from an org invite, it combines them with the token response policies to evaluate the user's password as weak", async () => {
|
||||
@@ -242,12 +247,6 @@ describe("PasswordLoginStrategy", () => {
|
||||
policyService.evaluateMasterPassword.mockReturnValue(false);
|
||||
tokenService.decodeAccessToken.mockResolvedValue({ sub: userId });
|
||||
|
||||
jest
|
||||
.spyOn(configService, "getFeatureFlag")
|
||||
.mockImplementation((flag: FeatureFlag) =>
|
||||
Promise.resolve(flag === FeatureFlag.PM16117_ChangeExistingPasswordRefactor),
|
||||
);
|
||||
|
||||
credentials.masterPasswordPoliciesFromOrgInvite = Object.assign(
|
||||
new MasterPasswordPolicyOptions(),
|
||||
{
|
||||
@@ -296,9 +295,16 @@ describe("PasswordLoginStrategy", () => {
|
||||
|
||||
it("forces the user to update their master password on successful login when it does not meet master password policy requirements", async () => {
|
||||
passwordStrengthService.getPasswordStrength.mockReturnValue({ score: 0 } as any);
|
||||
policyService.evaluateMasterPassword.mockReturnValue(false);
|
||||
tokenService.decodeAccessToken.mockResolvedValue({ sub: userId });
|
||||
|
||||
const combinedMasterPasswordPolicyOptions = Object.assign(new MasterPasswordPolicyOptions(), {
|
||||
enforceOnLogin: true,
|
||||
});
|
||||
policyService.combineMasterPasswordPolicyOptions.mockReturnValue(
|
||||
combinedMasterPasswordPolicyOptions,
|
||||
);
|
||||
policyService.evaluateMasterPassword.mockReturnValue(false);
|
||||
|
||||
await passwordLoginStrategy.logIn(credentials);
|
||||
|
||||
expect(policyService.evaluateMasterPassword).toHaveBeenCalled();
|
||||
@@ -330,9 +336,16 @@ describe("PasswordLoginStrategy", () => {
|
||||
|
||||
it("forces the user to update their master password on successful 2FA login when it does not meet master password policy requirements", async () => {
|
||||
passwordStrengthService.getPasswordStrength.mockReturnValue({ score: 0 } as any);
|
||||
policyService.evaluateMasterPassword.mockReturnValue(false);
|
||||
tokenService.decodeAccessToken.mockResolvedValue({ sub: userId });
|
||||
|
||||
const combinedMasterPasswordPolicyOptions = Object.assign(new MasterPasswordPolicyOptions(), {
|
||||
enforceOnLogin: true,
|
||||
});
|
||||
policyService.combineMasterPasswordPolicyOptions.mockReturnValue(
|
||||
combinedMasterPasswordPolicyOptions,
|
||||
);
|
||||
policyService.evaluateMasterPassword.mockReturnValue(false);
|
||||
|
||||
const token2FAResponse = new IdentityTwoFactorResponse({
|
||||
TwoFactorProviders: ["0"],
|
||||
TwoFactorProviders2: { 0: null },
|
||||
|
||||
@@ -12,7 +12,6 @@ import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/ide
|
||||
import { IdentityDeviceVerificationResponse } from "@bitwarden/common/auth/models/response/identity-device-verification.response";
|
||||
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
|
||||
import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { HashPurpose } from "@bitwarden/common/platform/enums";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||
@@ -171,35 +170,22 @@ export class PasswordLoginStrategy extends LoginStrategy {
|
||||
return;
|
||||
}
|
||||
|
||||
// The identity result can contain master password policies for the user's organizations
|
||||
let masterPasswordPolicyOptions: MasterPasswordPolicyOptions | undefined;
|
||||
// The identity result can contain master password policies for the user's organizations.
|
||||
// Get the master password policy options from both the org invite and the identity response.
|
||||
const masterPasswordPolicyOptions = this.policyService.combineMasterPasswordPolicyOptions(
|
||||
credentials.masterPasswordPoliciesFromOrgInvite,
|
||||
this.getMasterPasswordPolicyOptionsFromResponse(identityResponse),
|
||||
);
|
||||
|
||||
// We deliberately do not check enforceOnLogin as existing users who are logging
|
||||
// in after getting an org invite should always be forced to set a password that
|
||||
// meets the org's policy. Org Invite -> Registration also works this way for
|
||||
// new BW users as well.
|
||||
if (
|
||||
await this.configService.getFeatureFlag(FeatureFlag.PM16117_ChangeExistingPasswordRefactor)
|
||||
!credentials.masterPasswordPoliciesFromOrgInvite &&
|
||||
!masterPasswordPolicyOptions?.enforceOnLogin
|
||||
) {
|
||||
// Get the master password policy options from both the org invite and the identity response.
|
||||
masterPasswordPolicyOptions = this.policyService.combineMasterPasswordPolicyOptions(
|
||||
credentials.masterPasswordPoliciesFromOrgInvite,
|
||||
this.getMasterPasswordPolicyOptionsFromResponse(identityResponse),
|
||||
);
|
||||
|
||||
// We deliberately do not check enforceOnLogin as existing users who are logging
|
||||
// in after getting an org invite should always be forced to set a password that
|
||||
// meets the org's policy. Org Invite -> Registration also works this way for
|
||||
// new BW users as well.
|
||||
if (
|
||||
!credentials.masterPasswordPoliciesFromOrgInvite &&
|
||||
!masterPasswordPolicyOptions?.enforceOnLogin
|
||||
) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
masterPasswordPolicyOptions =
|
||||
this.getMasterPasswordPolicyOptionsFromResponse(identityResponse);
|
||||
|
||||
if (!masterPasswordPolicyOptions?.enforceOnLogin) {
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// If there is a policy active, evaluate the supplied password before its no longer in memory
|
||||
|
||||
@@ -10,7 +10,6 @@ import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth
|
||||
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
|
||||
import { IUserDecryptionOptionsServerResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/user-decryption-options.response";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncryptedString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||
@@ -83,6 +82,7 @@ describe("SsoLoginStrategy", () => {
|
||||
const ssoCodeVerifier = "SSO_CODE_VERIFIER";
|
||||
const ssoRedirectUrl = "SSO_REDIRECT_URL";
|
||||
const ssoOrgId = "SSO_ORG_ID";
|
||||
const privateKey = "userKeyEncryptedPrivateKey";
|
||||
|
||||
beforeEach(async () => {
|
||||
accountService = mockAccountServiceWith(userId);
|
||||
@@ -114,6 +114,9 @@ describe("SsoLoginStrategy", () => {
|
||||
tokenService.decodeAccessToken.mockResolvedValue({
|
||||
sub: userId,
|
||||
});
|
||||
keyService.userEncryptedPrivateKey$
|
||||
.calledWith(userId)
|
||||
.mockReturnValue(of(privateKey as EncryptedString));
|
||||
|
||||
const mockVaultTimeoutAction = VaultTimeoutAction.Lock;
|
||||
const mockVaultTimeoutActionBSub = new BehaviorSubject<VaultTimeoutAction>(
|
||||
@@ -163,6 +166,7 @@ describe("SsoLoginStrategy", () => {
|
||||
|
||||
it("sends SSO information to server", async () => {
|
||||
apiService.postIdentityToken.mockResolvedValue(identityTokenResponseFactory());
|
||||
keyService.hasUserKey.mockResolvedValue(true);
|
||||
|
||||
await ssoLoginStrategy.logIn(credentials);
|
||||
|
||||
@@ -185,6 +189,7 @@ describe("SsoLoginStrategy", () => {
|
||||
it("does not set keys for new SSO user flow", async () => {
|
||||
const tokenResponse = identityTokenResponseFactory();
|
||||
tokenResponse.key = null;
|
||||
tokenResponse.privateKey = null;
|
||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||
|
||||
await ssoLoginStrategy.logIn(credentials);
|
||||
@@ -210,42 +215,28 @@ describe("SsoLoginStrategy", () => {
|
||||
);
|
||||
});
|
||||
|
||||
describe("given the PM16117_SetInitialPasswordRefactor feature flag is ON", () => {
|
||||
beforeEach(() => {
|
||||
configService.getFeatureFlag.mockImplementation(async (flag) => {
|
||||
if (flag === FeatureFlag.PM16117_SetInitialPasswordRefactor) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
});
|
||||
describe("given the user does not have the `trustedDeviceOption`, does not have a master password, is not using key connector, does not have a user key, but they DO have a `userKeyEncryptedPrivateKey`", () => {
|
||||
it("should set the forceSetPasswordReason to TdeOffboardingUntrustedDevice", async () => {
|
||||
// Arrange
|
||||
const mockUserDecryptionOptions: IUserDecryptionOptionsServerResponse = {
|
||||
HasMasterPassword: false,
|
||||
TrustedDeviceOption: null,
|
||||
KeyConnectorOption: null,
|
||||
};
|
||||
const tokenResponse = identityTokenResponseFactory(null, mockUserDecryptionOptions);
|
||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||
|
||||
describe("given the user does not have the `trustedDeviceOption`, does not have a master password, is not using key connector, does not have a user key, but they DO have a `userKeyEncryptedPrivateKey`", () => {
|
||||
it("should set the forceSetPasswordReason to TdeOffboardingUntrustedDevice", async () => {
|
||||
// Arrange
|
||||
const mockUserDecryptionOptions: IUserDecryptionOptionsServerResponse = {
|
||||
HasMasterPassword: false,
|
||||
TrustedDeviceOption: null,
|
||||
KeyConnectorOption: null,
|
||||
};
|
||||
const tokenResponse = identityTokenResponseFactory(null, mockUserDecryptionOptions);
|
||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||
keyService.hasUserKey.mockResolvedValue(false);
|
||||
|
||||
keyService.userEncryptedPrivateKey$.mockReturnValue(
|
||||
of("userKeyEncryptedPrivateKey" as EncryptedString),
|
||||
);
|
||||
keyService.hasUserKey.mockResolvedValue(false);
|
||||
// Act
|
||||
await ssoLoginStrategy.logIn(credentials);
|
||||
|
||||
// Act
|
||||
await ssoLoginStrategy.logIn(credentials);
|
||||
|
||||
// Assert
|
||||
expect(masterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledTimes(1);
|
||||
expect(masterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledWith(
|
||||
ForceSetPasswordReason.TdeOffboardingUntrustedDevice,
|
||||
userId,
|
||||
);
|
||||
});
|
||||
// Assert
|
||||
expect(masterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledTimes(1);
|
||||
expect(masterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledWith(
|
||||
ForceSetPasswordReason.TdeOffboardingUntrustedDevice,
|
||||
userId,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import { SsoTokenRequest } from "@bitwarden/common/auth/models/request/identity-
|
||||
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
|
||||
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
|
||||
import { HttpStatusCode } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
@@ -344,38 +343,18 @@ export class SsoLoginStrategy extends LoginStrategy {
|
||||
tokenResponse: IdentityTokenResponse,
|
||||
userId: UserId,
|
||||
): Promise<void> {
|
||||
const isSetInitialPasswordFlagOn = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.PM16117_SetInitialPasswordRefactor,
|
||||
);
|
||||
|
||||
if (isSetInitialPasswordFlagOn) {
|
||||
if (tokenResponse.hasMasterKeyEncryptedUserKey()) {
|
||||
// User has masterKeyEncryptedUserKey, so set the userKeyEncryptedPrivateKey
|
||||
// Note: new JIT provisioned SSO users will not yet have a user asymmetric key pair
|
||||
// and so we don't want them falling into the createKeyPairForOldAccount flow
|
||||
await this.keyService.setPrivateKey(
|
||||
tokenResponse.privateKey ?? (await this.createKeyPairForOldAccount(userId)),
|
||||
userId,
|
||||
);
|
||||
} else if (tokenResponse.privateKey) {
|
||||
// User doesn't have masterKeyEncryptedUserKey but they do have a userKeyEncryptedPrivateKey
|
||||
// This is just existing TDE users or a TDE offboarder on an untrusted device
|
||||
await this.keyService.setPrivateKey(tokenResponse.privateKey, userId);
|
||||
}
|
||||
// else {
|
||||
// User could be new JIT provisioned SSO user in either a MP encryption org OR a TDE org.
|
||||
// In either case, the user doesn't yet have a user asymmetric key pair, a user key, or a master key + master key encrypted user key.
|
||||
// }
|
||||
} else {
|
||||
// A user that does not yet have a masterKeyEncryptedUserKey set is a new SSO user
|
||||
const newSsoUser = tokenResponse.key == null;
|
||||
|
||||
if (!newSsoUser) {
|
||||
await this.keyService.setPrivateKey(
|
||||
tokenResponse.privateKey ?? (await this.createKeyPairForOldAccount(userId)),
|
||||
userId,
|
||||
);
|
||||
}
|
||||
if (tokenResponse.hasMasterKeyEncryptedUserKey()) {
|
||||
// User has masterKeyEncryptedUserKey, so set the userKeyEncryptedPrivateKey
|
||||
// Note: new JIT provisioned SSO users will not yet have a user asymmetric key pair
|
||||
// and so we don't want them falling into the createKeyPairForOldAccount flow
|
||||
await this.keyService.setPrivateKey(
|
||||
tokenResponse.privateKey ?? (await this.createKeyPairForOldAccount(userId)),
|
||||
userId,
|
||||
);
|
||||
} else if (tokenResponse.privateKey) {
|
||||
// User doesn't have masterKeyEncryptedUserKey but they do have a userKeyEncryptedPrivateKey
|
||||
// This is just existing TDE users or a TDE offboarder on an untrusted device
|
||||
await this.keyService.setPrivateKey(tokenResponse.privateKey, userId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -431,30 +410,25 @@ export class SsoLoginStrategy extends LoginStrategy {
|
||||
// - UserDecryptionOptions.UsesKeyConnector is undefined. -- they aren't using key connector
|
||||
// - UserKey is not set after successful login -- because automatic decryption is not available
|
||||
// - userKeyEncryptedPrivateKey is set after successful login -- this is the key differentiator between a TDE org user logging into an untrusted device and MP encryption JIT provisioned user logging in for the first time.
|
||||
const isSetInitialPasswordFlagOn = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.PM16117_SetInitialPasswordRefactor,
|
||||
// Why is that the case? Because we set the userKeyEncryptedPrivateKey when we create the userKey, and this is serving as a proxy to tell us that the userKey has been created already (when enrolling in TDE).
|
||||
const hasUserKeyEncryptedPrivateKey = await firstValueFrom(
|
||||
this.keyService.userEncryptedPrivateKey$(userId),
|
||||
);
|
||||
const hasUserKey = await this.keyService.hasUserKey(userId);
|
||||
|
||||
if (isSetInitialPasswordFlagOn) {
|
||||
const hasUserKeyEncryptedPrivateKey = await firstValueFrom(
|
||||
this.keyService.userEncryptedPrivateKey$(userId),
|
||||
// TODO: PM-23491 we should explore consolidating this logic into a flag on the server. It could be set when an org is switched from TDE to MP encryption for each org user.
|
||||
if (
|
||||
!userDecryptionOptions.trustedDeviceOption &&
|
||||
!userDecryptionOptions.hasMasterPassword &&
|
||||
!userDecryptionOptions.keyConnectorOption?.keyConnectorUrl &&
|
||||
hasUserKeyEncryptedPrivateKey &&
|
||||
!hasUserKey
|
||||
) {
|
||||
await this.masterPasswordService.setForceSetPasswordReason(
|
||||
ForceSetPasswordReason.TdeOffboardingUntrustedDevice,
|
||||
userId,
|
||||
);
|
||||
const hasUserKey = await this.keyService.hasUserKey(userId);
|
||||
|
||||
// TODO: PM-23491 we should explore consolidating this logic into a flag on the server. It could be set when an org is switched from TDE to MP encryption for each org user.
|
||||
if (
|
||||
!userDecryptionOptions.trustedDeviceOption &&
|
||||
!userDecryptionOptions.hasMasterPassword &&
|
||||
!userDecryptionOptions.keyConnectorOption?.keyConnectorUrl &&
|
||||
hasUserKeyEncryptedPrivateKey &&
|
||||
!hasUserKey
|
||||
) {
|
||||
await this.masterPasswordService.setForceSetPasswordReason(
|
||||
ForceSetPasswordReason.TdeOffboardingUntrustedDevice,
|
||||
userId,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if user has permission to set password but hasn't yet
|
||||
|
||||
@@ -89,8 +89,7 @@ export class DefaultPolicyService implements PolicyService {
|
||||
const policies$ = policies ? of(policies) : this.policies$(userId);
|
||||
return policies$.pipe(
|
||||
map((obsPolicies) => {
|
||||
// TODO: replace with this.combinePoliciesIntoMasterPasswordPolicyOptions(obsPolicies)) once
|
||||
// FeatureFlag.PM16117_ChangeExistingPasswordRefactor is removed.
|
||||
// TODO ([PM-23777]): replace with this.combinePoliciesIntoMasterPasswordPolicyOptions(obsPolicies))
|
||||
let enforcedOptions: MasterPasswordPolicyOptions | undefined = undefined;
|
||||
const filteredPolicies =
|
||||
obsPolicies.filter((p) => p.type === PolicyType.MasterPassword) ?? [];
|
||||
|
||||
@@ -14,8 +14,6 @@ export enum FeatureFlag {
|
||||
CreateDefaultLocation = "pm-19467-create-default-location",
|
||||
|
||||
/* Auth */
|
||||
PM16117_SetInitialPasswordRefactor = "pm-16117-set-initial-password-refactor",
|
||||
PM16117_ChangeExistingPasswordRefactor = "pm-16117-change-existing-password-refactor",
|
||||
PM14938_BrowserExtensionLoginApproval = "pm-14938-browser-extension-login-approvals",
|
||||
|
||||
/* Autofill */
|
||||
@@ -107,8 +105,6 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.PM22136_SdkCipherEncryption]: FALSE,
|
||||
|
||||
/* Auth */
|
||||
[FeatureFlag.PM16117_SetInitialPasswordRefactor]: FALSE,
|
||||
[FeatureFlag.PM16117_ChangeExistingPasswordRefactor]: FALSE,
|
||||
[FeatureFlag.PM14938_BrowserExtensionLoginApproval]: FALSE,
|
||||
|
||||
/* Billing */
|
||||
|
||||
Reference in New Issue
Block a user