mirror of
https://github.com/bitwarden/browser
synced 2026-02-19 02:44:01 +00:00
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`
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<void> {
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<void>;
|
||||
|
||||
/**
|
||||
* 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<void>;
|
||||
|
||||
/**
|
||||
* 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:
|
||||
|
||||
Reference in New Issue
Block a user