From 4a651fbfb3fbbf3ad73fabfd25fef63e83ada161 Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Tue, 17 Feb 2026 10:44:21 -0800 Subject: [PATCH] refactor(input-password-flows) [Auth/PM-27086] Use new KM Data Types in InputPasswordComponent flows - TDE & Permission User (#18400) Updates the SetInitialPasswordService TDE + Permission user flow to use the new KM data types: - `MasterPasswordAuthenticationData` - `MasterPasswordUnlockData` This allows us to move away from the deprecated `makeMasterKey()` method (which takes email as salt) as we seek to eventually separate the email from the salt. The new `setInitialPasswordTdeUserWithPermission()` method essentially takes the existing deprecated `setInitialPassword()` method and: - Removes logic that is specific to a `JIT_PROVISIONED_MP_ORG_USER` case. This way the method only handles `TDE_ORG_USER_RESET_PASSWORD_PERMISSION_REQUIRES_MP` cases. - Updates the logic to use `MasterPasswordAuthenticationData` and `MasterPasswordUnlockData` Behind feature flag: `pm-27086-update-authentication-apis-for-input-password` --- ...sktop-set-initial-password.service.spec.ts | 66 ++++ .../desktop-set-initial-password.service.ts | 10 + ...initial-password.service.implementation.ts | 138 ++++++++- ...fault-set-initial-password.service.spec.ts | 287 +++++++++++++++++- .../set-initial-password.component.ts | 47 +++ ...et-initial-password.service.abstraction.ts | 23 ++ 6 files changed, 568 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/app/services/set-initial-password/desktop-set-initial-password.service.spec.ts b/apps/desktop/src/app/services/set-initial-password/desktop-set-initial-password.service.spec.ts index 430870a247b..6ceb2871b3f 100644 --- a/apps/desktop/src/app/services/set-initial-password/desktop-set-initial-password.service.spec.ts +++ b/apps/desktop/src/app/services/set-initial-password/desktop-set-initial-password.service.spec.ts @@ -7,6 +7,7 @@ import { InitializeJitPasswordCredentials, SetInitialPasswordCredentials, SetInitialPasswordService, + SetInitialPasswordTdeUserWithPermissionCredentials, SetInitialPasswordUserType, } from "@bitwarden/angular/auth/password-management/set-initial-password/set-initial-password.service.abstraction"; import { @@ -30,6 +31,7 @@ import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/sym import { CsprngArray } from "@bitwarden/common/types/csprng"; import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; +import { newGuid } from "@bitwarden/guid"; import { DEFAULT_KDF_CONFIG, KdfConfigService, KeyService } from "@bitwarden/key-management"; import { DesktopSetInitialPasswordService } from "./desktop-set-initial-password.service"; @@ -224,4 +226,68 @@ describe("DesktopSetInitialPasswordService", () => { superSpy.mockRestore(); }); }); + + describe("setInitialPasswordTdeUserWithPermission()", () => { + let credentials: SetInitialPasswordTdeUserWithPermissionCredentials; + let userId: UserId; + let superSpy: jest.SpyInstance; + + beforeEach(() => { + credentials = { + newPassword: "newPassword123!", + salt: "user@example.com" as MasterPasswordSalt, + kdfConfig: DEFAULT_KDF_CONFIG, + newPasswordHint: "newPasswordHint", + orgSsoIdentifier: "orgSsoIdentifier", + orgId: "orgId" as OrganizationId, + resetPasswordAutoEnroll: false, + }; + userId = newGuid() as UserId; + + superSpy = jest + .spyOn( + DefaultSetInitialPasswordService.prototype, + "setInitialPasswordTdeUserWithPermission", + ) + .mockResolvedValue(undefined); // undefined = successful + }); + + afterEach(() => { + superSpy.mockRestore(); + }); + + it("should call the setInitialPasswordTdeUserWithPermission() method on the default service", async () => { + // Act + await sut.setInitialPasswordTdeUserWithPermission(credentials, userId); + + // Assert + expect(superSpy).toHaveBeenCalledWith(credentials, userId); + }); + + describe("given the initial password was successfully set", () => { + it("should send a 'redrawMenu' message", async () => { + // Act + await sut.setInitialPasswordTdeUserWithPermission(credentials, userId); + + // Assert + expect(messagingService.send).toHaveBeenCalledTimes(1); + expect(messagingService.send).toHaveBeenCalledWith("redrawMenu"); + }); + }); + + describe("given the initial password was NOT successfully set (due an error on the default service)", () => { + it("should NOT send a 'redrawMenu' message", async () => { + // Arrange + const error = new Error("error on DefaultSetInitialPasswordService"); + superSpy.mockRejectedValue(error); + + // Act + const promise = sut.setInitialPasswordTdeUserWithPermission(credentials, userId); + + // Assert + await expect(promise).rejects.toThrow(error); + expect(messagingService.send).not.toHaveBeenCalled(); + }); + }); + }); }); diff --git a/apps/desktop/src/app/services/set-initial-password/desktop-set-initial-password.service.ts b/apps/desktop/src/app/services/set-initial-password/desktop-set-initial-password.service.ts index 3b1562075f9..b03d87870f9 100644 --- a/apps/desktop/src/app/services/set-initial-password/desktop-set-initial-password.service.ts +++ b/apps/desktop/src/app/services/set-initial-password/desktop-set-initial-password.service.ts @@ -4,6 +4,7 @@ import { InitializeJitPasswordCredentials, SetInitialPasswordCredentials, SetInitialPasswordService, + SetInitialPasswordTdeUserWithPermissionCredentials, SetInitialPasswordUserType, } from "@bitwarden/angular/auth/password-management/set-initial-password/set-initial-password.service.abstraction"; import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; @@ -75,4 +76,13 @@ export class DesktopSetInitialPasswordService this.messagingService.send("redrawMenu"); } + + override async setInitialPasswordTdeUserWithPermission( + credentials: SetInitialPasswordTdeUserWithPermissionCredentials, + userId: UserId, + ) { + await super.setInitialPasswordTdeUserWithPermission(credentials, userId); + + this.messagingService.send("redrawMenu"); + } } diff --git a/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.implementation.ts b/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.implementation.ts index d83e40d1d44..317030c25aa 100644 --- a/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.implementation.ts +++ b/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.implementation.ts @@ -15,11 +15,13 @@ import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/ma import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request"; import { UpdateTdeOffboardingPasswordRequest } from "@bitwarden/common/auth/models/request/update-tde-offboarding-password.request"; +import { assertNonNullish, assertTruthy } from "@bitwarden/common/auth/utils"; import { AccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/account-cryptographic-state.service"; 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 { + MasterPasswordAuthenticationData, MasterPasswordSalt, MasterPasswordUnlockData, } from "@bitwarden/common/key-management/master-password/types/master-password.types"; @@ -45,6 +47,7 @@ import { SetInitialPasswordService, SetInitialPasswordTdeOffboardingCredentials, SetInitialPasswordUserType, + SetInitialPasswordTdeUserWithPermissionCredentials, } from "./set-initial-password.service.abstraction"; export class DefaultSetInitialPasswordService implements SetInitialPasswordService { @@ -212,7 +215,7 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi await this.masterPasswordService.setMasterKeyHash(newLocalMasterKeyHash, userId); if (resetPasswordAutoEnroll) { - await this.handleResetPasswordAutoEnroll(newServerMasterKeyHash, orgId, userId); + await this.handleResetPasswordAutoEnrollOld(newServerMasterKeyHash, orgId, userId); } } @@ -336,6 +339,86 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi ); } + async setInitialPasswordTdeUserWithPermission( + credentials: SetInitialPasswordTdeUserWithPermissionCredentials, + userId: UserId, + ): Promise { + const ctx = + "Could not set initial password for TDE user with Manage Account Recovery permission."; + + assertTruthy(credentials.newPassword, "newPassword", ctx); + assertTruthy(credentials.salt, "salt", ctx); + assertNonNullish(credentials.kdfConfig, "kdfConfig", ctx); + assertNonNullish(credentials.newPasswordHint, "newPasswordHint", ctx); // can have an empty string as a valid value, so check non-nullish + assertTruthy(credentials.orgSsoIdentifier, "orgSsoIdentifier", ctx); + assertTruthy(credentials.orgId, "orgId", ctx); + assertNonNullish(credentials.resetPasswordAutoEnroll, "resetPasswordAutoEnroll", ctx); // can have `false` as a valid value, so check non-nullish + assertTruthy(userId, "userId", ctx); + + const { + newPassword, + salt, + kdfConfig, + newPasswordHint, + orgSsoIdentifier, + orgId, + resetPasswordAutoEnroll, + } = credentials; + + const userKey = await firstValueFrom(this.keyService.userKey$(userId)); + + if (!userKey) { + throw new Error("userKey not found."); + } + + const authenticationData: MasterPasswordAuthenticationData = + await this.masterPasswordService.makeMasterPasswordAuthenticationData( + newPassword, + kdfConfig, + salt, + ); + + const unlockData: MasterPasswordUnlockData = + await this.masterPasswordService.makeMasterPasswordUnlockData( + newPassword, + kdfConfig, + salt, + userKey, + ); + + const request = SetPasswordRequest.newConstructor( + authenticationData, + unlockData, + newPasswordHint, + orgSsoIdentifier, + null, // no KeysRequest for TDE user because they already have a key pair + ); + + 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 decryption state + await this.masterPasswordService.setMasterPasswordUnlockData(unlockData, userId); + await this.updateLegacyState( + newPassword, + unlockData.kdf, + new EncString(unlockData.masterKeyWrappedUserKey), + userId, + unlockData, + ); + + if (resetPasswordAutoEnroll) { + await this.handleResetPasswordAutoEnroll( + authenticationData.masterPasswordAuthenticationHash, + orgId, + userId, + userKey, + ); + } + } + /** * @deprecated To be removed in PM-28143 */ @@ -441,7 +524,19 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi await this.masterPasswordService.setMasterPasswordUnlockData(masterPasswordUnlockData, userId); } - private async handleResetPasswordAutoEnroll( + /** + * @deprecated To be removed in PM-28143 + * + * This method is now deprecated because it is used with the deprecated `setInitialPassword()` method, + * which handles both JIT MP and TDE + Permission user flows. + * + * Since these methods can handle the JIT MP flow - which creates a new user key and sets it to state - we + * must retreive that user key here in this method. + * + * But the new handleResetPasswordAutoEnroll() method is only used in the TDE + Permission user case, in which + * case we already have the user key and can simply pass it through via method parameter ( @see handleResetPasswordAutoEnroll ) + */ + private async handleResetPasswordAutoEnrollOld( masterKeyHash: string, orgId: string, userId: UserId, @@ -483,4 +578,43 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi enrollmentRequest, ); } + + private async handleResetPasswordAutoEnroll( + masterKeyHash: string, + orgId: string, + userId: UserId, + userKey: UserKey, + ) { + const organizationKeys = await this.organizationApiService.getKeys(orgId); + + if (organizationKeys == null) { + throw new Error( + "Organization keys response is null. Could not handle reset password auto enroll.", + ); + } + + const orgPublicKey = Utils.fromB64ToArray(organizationKeys.publicKey); + + // RSA encrypt user key with organization public key + const orgPublicKeyEncryptedUserKey = await this.encryptService.encapsulateKeyUnsigned( + userKey, + orgPublicKey, + ); + + if (orgPublicKeyEncryptedUserKey == null || !orgPublicKeyEncryptedUserKey.encryptedString) { + throw new Error( + "orgPublicKeyEncryptedUserKey not found. Could not handle reset password auto enroll.", + ); + } + + const enrollmentRequest = new OrganizationUserResetPasswordEnrollmentRequest(); + enrollmentRequest.masterPasswordHash = masterKeyHash; + enrollmentRequest.resetPasswordKey = orgPublicKeyEncryptedUserKey.encryptedString; + + await this.organizationUserApiService.putOrganizationUserResetPasswordEnrollment( + orgId, + userId, + enrollmentRequest, + ); + } } diff --git a/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.spec.ts b/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.spec.ts index 8b64e20ce7b..d68bf2c7d01 100644 --- a/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.spec.ts +++ b/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.spec.ts @@ -31,6 +31,9 @@ import { } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; import { + MasterKeyWrappedUserKey, + MasterPasswordAuthenticationData, + MasterPasswordAuthenticationHash, MasterPasswordSalt, MasterPasswordUnlockData, } from "@bitwarden/common/key-management/master-password/types/master-password.types"; @@ -62,6 +65,7 @@ import { SetInitialPasswordCredentials, SetInitialPasswordService, SetInitialPasswordTdeOffboardingCredentials, + SetInitialPasswordTdeUserWithPermissionCredentials, SetInitialPasswordUserType, } from "./set-initial-password.service.abstraction"; @@ -237,7 +241,7 @@ describe("DefaultSetInitialPasswordService", () => { } } - // Mock handleResetPasswordAutoEnroll() values + // Mock handleResetPasswordAutoEnrollOld() values if (config.resetPasswordAutoEnroll) { organizationApiService.getKeys.mockResolvedValue(organizationKeys); encryptService.encapsulateKeyUnsigned.mockResolvedValue(orgPublicKeyEncryptedUserKey); @@ -1104,4 +1108,285 @@ describe("DefaultSetInitialPasswordService", () => { await expect(promise).rejects.toThrow("Unexpected V2 account cryptographic state"); }); }); + + describe("setInitialPasswordTdeUserWithPermission()", () => { + // Mock method parameters + let credentials: SetInitialPasswordTdeUserWithPermissionCredentials; + + // Mock method data + let authenticationData: MasterPasswordAuthenticationData; + let unlockData: MasterPasswordUnlockData; + let setPasswordRequest: SetPasswordRequest; + let userDecryptionOptions: UserDecryptionOptions; + + beforeEach(() => { + // Mock method parameters + credentials = { + newPassword: "newPassword123!", + salt: "user@example.com" as MasterPasswordSalt, + kdfConfig: DEFAULT_KDF_CONFIG, + newPasswordHint: "newPasswordHint", + orgSsoIdentifier: "orgSsoIdentifier", + orgId: "orgId" as OrganizationId, + resetPasswordAutoEnroll: false, + }; + + // Mock method data + userKey = makeSymmetricCryptoKey(64) as UserKey; + keyService.userKey$.mockReturnValue(of(userKey)); + + authenticationData = { + salt: credentials.salt, + kdf: credentials.kdfConfig, + masterPasswordAuthenticationHash: + "masterPasswordAuthenticationHash" as MasterPasswordAuthenticationHash, + }; + masterPasswordService.makeMasterPasswordAuthenticationData.mockResolvedValue( + authenticationData, + ); + + unlockData = { + salt: credentials.salt, + kdf: credentials.kdfConfig, + masterKeyWrappedUserKey: "masterKeyWrappedUserKey" as MasterKeyWrappedUserKey, + } as MasterPasswordUnlockData; + masterPasswordService.makeMasterPasswordUnlockData.mockResolvedValue(unlockData); + + setPasswordRequest = SetPasswordRequest.newConstructor( + authenticationData, + unlockData, + credentials.newPasswordHint, + credentials.orgSsoIdentifier, + null, // no KeysRequest for TDE user because they already have a key pair + ); + + userDecryptionOptions = new UserDecryptionOptions({ hasMasterPassword: false }); + userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue( + of(userDecryptionOptions), + ); + }); + + describe("general error handling", () => { + ["newPassword", "salt", "orgSsoIdentifier", "orgId"].forEach((key) => { + it(`should throw if ${key} is an empty string (falsy) on the SetInitialPasswordTdeUserWithPermissionCredentials object`, async () => { + // Arrange + const invalidCredentials: SetInitialPasswordTdeUserWithPermissionCredentials = { + ...credentials, + [key]: "", + }; + + // Act + const promise = sut.setInitialPasswordTdeUserWithPermission(invalidCredentials, userId); + + // Assert + await expect(promise).rejects.toThrow( + `${key} is falsy. Could not set initial password for TDE user with Manage Account Recovery permission.`, + ); + }); + }); + + ["kdfConfig", "newPasswordHint", "resetPasswordAutoEnroll"].forEach((key) => { + it(`should throw if ${key} is null on the SetInitialPasswordTdeUserWithPermissionCredentials object`, async () => { + // Arrange + const invalidCredentials: SetInitialPasswordTdeUserWithPermissionCredentials = { + ...credentials, + [key]: null, + }; + + // Act + const promise = sut.setInitialPasswordTdeUserWithPermission(invalidCredentials, userId); + + // Assert + await expect(promise).rejects.toThrow( + `${key} is null or undefined. Could not set initial password for TDE user with Manage Account Recovery permission.`, + ); + }); + }); + + it("should throw if userId is not given", async () => { + // Arrange + userId = null; + + // Act + const promise = sut.setInitialPasswordTdeUserWithPermission(credentials, userId); + + // Assert + await expect(promise).rejects.toThrow( + "userId is falsy. Could not set initial password for TDE user with Manage Account Recovery permission.", + ); + }); + }); + + it("should throw if the userKey is not found", async () => { + // Arrange + keyService.userKey$.mockReturnValue(of(null)); + + // Act + const promise = sut.setInitialPasswordTdeUserWithPermission(credentials, userId); + + // Assert + await expect(promise).rejects.toThrow("userKey not found."); + }); + + it("should call makeMasterPasswordAuthenticationData and makeMasterPasswordUnlockData with the correct parameters", async () => { + // Act + await sut.setInitialPasswordTdeUserWithPermission(credentials, userId); + + // Assert + expect(masterPasswordService.makeMasterPasswordAuthenticationData).toHaveBeenCalledWith( + credentials.newPassword, + credentials.kdfConfig, + credentials.salt, + ); + + expect(masterPasswordService.makeMasterPasswordUnlockData).toHaveBeenCalledWith( + credentials.newPassword, + credentials.kdfConfig, + credentials.salt, + userKey, + ); + }); + + it("should call the API method to set a master password", async () => { + // Act + await sut.setInitialPasswordTdeUserWithPermission(credentials, userId); + + // Assert + expect(masterPasswordApiService.setPassword).toHaveBeenCalledTimes(1); + expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest); + }); + + describe("given the initial password has been successfully set", () => { + it("should clear the ForceSetPasswordReason by setting it to None", async () => { + // Act + await sut.setInitialPasswordTdeUserWithPermission(credentials, userId); + + // Assert + expect(masterPasswordService.setForceSetPasswordReason).toHaveBeenCalledWith( + ForceSetPasswordReason.None, + userId, + ); + }); + + it("should set MasterPasswordUnlockData to state", async () => { + // Act + await sut.setInitialPasswordTdeUserWithPermission(credentials, userId); + + // Assert + expect(masterPasswordService.setMasterPasswordUnlockData).toHaveBeenCalledWith( + unlockData, + userId, + ); + }); + + it("should update legacy state", async () => { + // Act + await sut.setInitialPasswordTdeUserWithPermission(credentials, userId); + + // Assert + expect(userDecryptionOptionsService.setUserDecryptionOptionsById).toHaveBeenCalledWith( + userId, + expect.objectContaining({ hasMasterPassword: true }), + ); + expect(kdfConfigService.setKdfConfig).toHaveBeenCalledWith(userId, credentials.kdfConfig); + expect(masterPasswordService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith( + new EncString(unlockData.masterKeyWrappedUserKey), + userId, + ); + expect(masterPasswordService.setLegacyMasterKeyFromUnlockData).toHaveBeenCalledWith( + credentials.newPassword, + unlockData, + userId, + ); + }); + + describe("given resetPasswordAutoEnroll is false", () => { + it("should NOT handle reset password (account recovery) auto enroll", async () => { + // Act + await sut.setInitialPasswordTdeUserWithPermission(credentials, userId); + + // Assert + expect( + organizationUserApiService.putOrganizationUserResetPasswordEnrollment, + ).not.toHaveBeenCalled(); + }); + }); + + describe("given resetPasswordAutoEnroll is true", () => { + let organizationKeys: OrganizationKeysResponse; + let orgPublicKeyEncryptedUserKey: EncString; + let enrollmentRequest: OrganizationUserResetPasswordEnrollmentRequest; + + beforeEach(() => { + credentials.resetPasswordAutoEnroll = true; + + organizationKeys = { + privateKey: "orgPrivateKey", + publicKey: "orgPublicKey", + } as OrganizationKeysResponse; + organizationApiService.getKeys.mockResolvedValue(organizationKeys); + + orgPublicKeyEncryptedUserKey = new EncString("orgPublicKeyEncryptedUserKey"); + encryptService.encapsulateKeyUnsigned.mockResolvedValue(orgPublicKeyEncryptedUserKey); + + enrollmentRequest = new OrganizationUserResetPasswordEnrollmentRequest(); + enrollmentRequest.masterPasswordHash = + authenticationData.masterPasswordAuthenticationHash; + enrollmentRequest.resetPasswordKey = orgPublicKeyEncryptedUserKey.encryptedString; + }); + + it("should throw if organization keys are not found", async () => { + // Arrange + organizationApiService.getKeys.mockResolvedValue(null); + + // Act + const promise = sut.setInitialPasswordTdeUserWithPermission(credentials, userId); + + // Assert + await expect(promise).rejects.toThrow( + "Organization keys response is null. Could not handle reset password auto enroll.", + ); + }); + + it("should throw if orgPublicKeyEncryptedUserKey is not found", async () => { + // Arrange + encryptService.encapsulateKeyUnsigned.mockResolvedValue(null); + + // Act + const promise = sut.setInitialPasswordTdeUserWithPermission(credentials, userId); + + // Assert + await expect(promise).rejects.toThrow( + "orgPublicKeyEncryptedUserKey not found. Could not handle reset password auto enroll.", + ); + }); + + it("should throw if orgPublicKeyEncryptedUserKey.encryptedString is not found", async () => { + // Arrange + orgPublicKeyEncryptedUserKey.encryptedString = null; + + // Act + const promise = sut.setInitialPasswordTdeUserWithPermission(credentials, userId); + + // Assert + await expect(promise).rejects.toThrow( + "orgPublicKeyEncryptedUserKey not found. Could not handle reset password auto enroll.", + ); + }); + + it("should call the API method to handle reset password (account recovery) auto enroll", async () => { + // Act + await sut.setInitialPasswordTdeUserWithPermission(credentials, userId); + + // Assert + expect( + organizationUserApiService.putOrganizationUserResetPasswordEnrollment, + ).toHaveBeenCalledTimes(1); + expect( + organizationUserApiService.putOrganizationUserResetPasswordEnrollment, + ).toHaveBeenCalledWith(credentials.orgId, userId, enrollmentRequest); + }); + }); + }); + }); }); diff --git a/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.component.ts b/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.component.ts index 7850a980eef..3cafbdb8ff8 100644 --- a/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.component.ts +++ b/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.component.ts @@ -47,6 +47,7 @@ import { SetInitialPasswordCredentials, SetInitialPasswordService, SetInitialPasswordTdeOffboardingCredentials, + SetInitialPasswordTdeUserWithPermissionCredentials, SetInitialPasswordUserType, } from "./set-initial-password.service.abstraction"; @@ -183,7 +184,13 @@ export class SetInitialPasswordComponent implements OnInit { break; } case SetInitialPasswordUserType.TDE_ORG_USER_RESET_PASSWORD_PERMISSION_REQUIRES_MP: + if (passwordInputResult.newApisWithInputPasswordFlagEnabled) { + await this.setInitialPasswordTdeUserWithPermission(passwordInputResult); + return; // EARLY RETURN for flagged logic + } + await this.setInitialPassword(passwordInputResult); + break; case SetInitialPasswordUserType.OFFBOARDED_TDE_ORG_USER: await this.setInitialPasswordTdeOffboarding(passwordInputResult); @@ -382,6 +389,46 @@ export class SetInitialPasswordComponent implements OnInit { } } + private async setInitialPasswordTdeUserWithPermission(passwordInputResult: PasswordInputResult) { + const ctx = + "Could not set initial password for TDE user with Manage Account Recovery permission."; + + assertTruthy(passwordInputResult.newPassword, "newPassword", ctx); + assertTruthy(passwordInputResult.salt, "salt", ctx); + assertNonNullish(passwordInputResult.kdfConfig, "kdfConfig", ctx); + assertNonNullish(passwordInputResult.newPasswordHint, "newPasswordHint", ctx); // can have an empty string as a valid value, so check non-nullish + assertTruthy(this.orgSsoIdentifier, "orgSsoIdentifier", ctx); + assertTruthy(this.orgId, "orgId", ctx); + assertNonNullish(this.resetPasswordAutoEnroll, "resetPasswordAutoEnroll", ctx); // can have `false` as a valid value, so check non-nullish + assertTruthy(this.userId, "userId", ctx); + + try { + const credentials: SetInitialPasswordTdeUserWithPermissionCredentials = { + newPassword: passwordInputResult.newPassword, + salt: passwordInputResult.salt, + kdfConfig: passwordInputResult.kdfConfig, + newPasswordHint: passwordInputResult.newPasswordHint, + orgSsoIdentifier: this.orgSsoIdentifier, + orgId: this.orgId as OrganizationId, + resetPasswordAutoEnroll: this.resetPasswordAutoEnroll, + }; + + await this.setInitialPasswordService.setInitialPasswordTdeUserWithPermission( + credentials, + this.userId, + ); + + this.showSuccessToastByUserType(); + + this.submitting = false; + await this.router.navigate(["vault"]); + } catch (e) { + this.logService.error("Error setting initial password", e); + this.validationService.showError(e); + this.submitting = false; + } + } + private async setInitialPasswordTdeOffboarding(passwordInputResult: PasswordInputResult) { const ctx = "Could not set initial password."; assertTruthy(passwordInputResult.newMasterKey, "newMasterKey", ctx); diff --git a/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.service.abstraction.ts b/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.service.abstraction.ts index 70318be3393..5a68b787e28 100644 --- a/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.service.abstraction.ts +++ b/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.service.abstraction.ts @@ -55,6 +55,16 @@ export interface SetInitialPasswordCredentials { salt: MasterPasswordSalt; } +export interface SetInitialPasswordTdeUserWithPermissionCredentials { + newPassword: string; + salt: MasterPasswordSalt; + kdfConfig: KdfConfig; + newPasswordHint: string; + orgSsoIdentifier: string; + orgId: OrganizationId; + resetPasswordAutoEnroll: boolean; +} + export interface SetInitialPasswordTdeOffboardingCredentials { newMasterKey: MasterKey; newServerMasterKeyHash: string; @@ -103,6 +113,19 @@ export abstract class SetInitialPasswordService { userId: UserId, ) => Promise; + /** + * Sets an initial password for an existing authed TDE user who has been given the + * Manage Account Recovery permission: + * - {@link SetInitialPasswordUserType.TDE_ORG_USER_RESET_PASSWORD_PERMISSION_REQUIRES_MP} + * + * @param credentials An object of the credentials needed to set the initial password + * @throws If any property on the `credentials` object not found, or if userKey is not found + */ + abstract setInitialPasswordTdeUserWithPermission: ( + credentials: SetInitialPasswordTdeUserWithPermissionCredentials, + userId: UserId, + ) => Promise; + /** * Sets an initial password for a user who logs in after their org offboarded from * trusted device encryption and is now a master-password-encryption org: