1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-10 21:33:27 +00:00

feat(set-initial-password): [Auth/PM-18784] SetInitialPasswordComponent Handle TDE Offboarding (#14861)

This PR makes it so that `SetInitialPasswordComponent` handles the TDE offboarding flow where an org user now needs to set an initial master password.

Feature flag: `PM16117_SetInitialPasswordRefactor`
This commit is contained in:
rr-bw
2025-07-02 07:23:45 -07:00
committed by GitHub
parent 1837974e0a
commit cc65f5efc6
8 changed files with 391 additions and 56 deletions

View File

@@ -14,6 +14,7 @@ import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-conso
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-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 { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request"; 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 { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { KeysRequest } from "@bitwarden/common/models/request/keys.request"; import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
@@ -28,6 +29,7 @@ import {
SetInitialPasswordService, SetInitialPasswordService,
SetInitialPasswordCredentials, SetInitialPasswordCredentials,
SetInitialPasswordUserType, SetInitialPasswordUserType,
SetInitialPasswordTdeOffboardingCredentials,
} from "./set-initial-password.service.abstraction"; } from "./set-initial-password.service.abstraction";
export class DefaultSetInitialPasswordService implements SetInitialPasswordService { export class DefaultSetInitialPasswordService implements SetInitialPasswordService {
@@ -245,4 +247,44 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi
enrollmentRequest, enrollmentRequest,
); );
} }
async setInitialPasswordTdeOffboarding(
credentials: SetInitialPasswordTdeOffboardingCredentials,
userId: UserId,
) {
const { newMasterKey, newServerMasterKeyHash, newPasswordHint } = credentials;
for (const [key, value] of Object.entries(credentials)) {
if (value == null) {
throw new Error(`${key} not found. Could not set password.`);
}
}
if (userId == null) {
throw new Error("userId not found. Could not set password.");
}
const userKey = await firstValueFrom(this.keyService.userKey$(userId));
if (userKey == null) {
throw new Error("userKey not found. Could not set password.");
}
const newMasterKeyEncryptedUserKey = await this.keyService.encryptUserKeyWithMasterKey(
newMasterKey,
userKey,
);
if (!newMasterKeyEncryptedUserKey[1].encryptedString) {
throw new Error("newMasterKeyEncryptedUserKey not found. Could not set password.");
}
const request = new UpdateTdeOffboardingPasswordRequest();
request.key = newMasterKeyEncryptedUserKey[1].encryptedString;
request.newMasterPasswordHash = newServerMasterKeyHash;
request.masterPasswordHint = newPasswordHint;
await this.masterPasswordApiService.putUpdateTdeOffboardingPassword(request);
// Clear force set password reason to allow navigation back to vault.
await this.masterPasswordService.setForceSetPasswordReason(ForceSetPasswordReason.None, userId);
}
} }

View File

@@ -19,6 +19,7 @@ import { OrganizationKeysResponse } from "@bitwarden/common/admin-console/models
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-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 { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request"; 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 { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { KeysRequest } from "@bitwarden/common/models/request/keys.request"; import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
@@ -35,6 +36,7 @@ import { DefaultSetInitialPasswordService } from "./default-set-initial-password
import { import {
SetInitialPasswordCredentials, SetInitialPasswordCredentials,
SetInitialPasswordService, SetInitialPasswordService,
SetInitialPasswordTdeOffboardingCredentials,
SetInitialPasswordUserType, SetInitialPasswordUserType,
} from "./set-initial-password.service.abstraction"; } from "./set-initial-password.service.abstraction";
@@ -52,6 +54,11 @@ describe("DefaultSetInitialPasswordService", () => {
let organizationUserApiService: MockProxy<OrganizationUserApiService>; let organizationUserApiService: MockProxy<OrganizationUserApiService>;
let userDecryptionOptionsService: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>; let userDecryptionOptionsService: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>;
let userId: UserId;
let userKey: UserKey;
let userKeyEncString: EncString;
let masterKeyEncryptedUserKey: [UserKey, EncString];
beforeEach(() => { beforeEach(() => {
apiService = mock<ApiService>(); apiService = mock<ApiService>();
encryptService = mock<EncryptService>(); encryptService = mock<EncryptService>();
@@ -64,6 +71,11 @@ describe("DefaultSetInitialPasswordService", () => {
organizationUserApiService = mock<OrganizationUserApiService>(); organizationUserApiService = mock<OrganizationUserApiService>();
userDecryptionOptionsService = mock<InternalUserDecryptionOptionsServiceAbstraction>(); userDecryptionOptionsService = mock<InternalUserDecryptionOptionsServiceAbstraction>();
userId = "userId" as UserId;
userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey;
userKeyEncString = new EncString("masterKeyEncryptedUserKey");
masterKeyEncryptedUserKey = [userKey, userKeyEncString];
sut = new DefaultSetInitialPasswordService( sut = new DefaultSetInitialPasswordService(
apiService, apiService,
encryptService, encryptService,
@@ -86,13 +98,8 @@ describe("DefaultSetInitialPasswordService", () => {
// Mock function parameters // Mock function parameters
let credentials: SetInitialPasswordCredentials; let credentials: SetInitialPasswordCredentials;
let userType: SetInitialPasswordUserType; let userType: SetInitialPasswordUserType;
let userId: UserId;
// Mock other function data // Mock other function data
let userKey: UserKey;
let userKeyEncString: EncString;
let masterKeyEncryptedUserKey: [UserKey, EncString];
let existingUserPublicKey: UserPublicKey; let existingUserPublicKey: UserPublicKey;
let existingUserPrivateKey: UserPrivateKey; let existingUserPrivateKey: UserPrivateKey;
let userKeyEncryptedPrivateKey: EncString; let userKeyEncryptedPrivateKey: EncString;
@@ -121,14 +128,9 @@ describe("DefaultSetInitialPasswordService", () => {
orgId: "orgId", orgId: "orgId",
resetPasswordAutoEnroll: false, resetPasswordAutoEnroll: false,
}; };
userId = "userId" as UserId;
userType = SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER; userType = SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER;
// Mock other function data // Mock other function data
userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey;
userKeyEncString = new EncString("masterKeyEncryptedUserKey");
masterKeyEncryptedUserKey = [userKey, userKeyEncString];
existingUserPublicKey = Utils.fromB64ToArray("existingUserPublicKey") as UserPublicKey; existingUserPublicKey = Utils.fromB64ToArray("existingUserPublicKey") as UserPublicKey;
existingUserPrivateKey = Utils.fromB64ToArray("existingUserPrivateKey") as UserPrivateKey; existingUserPrivateKey = Utils.fromB64ToArray("existingUserPrivateKey") as UserPrivateKey;
userKeyEncryptedPrivateKey = new EncString("userKeyEncryptedPrivateKey"); userKeyEncryptedPrivateKey = new EncString("userKeyEncryptedPrivateKey");
@@ -630,4 +632,114 @@ describe("DefaultSetInitialPasswordService", () => {
}); });
}); });
}); });
describe("setInitialPasswordTdeOffboarding(...)", () => {
// Mock function parameters
let credentials: SetInitialPasswordTdeOffboardingCredentials;
beforeEach(() => {
// Mock function parameters
credentials = {
newMasterKey: new SymmetricCryptoKey(new Uint8Array(32).buffer as CsprngArray) as MasterKey,
newServerMasterKeyHash: "newServerMasterKeyHash",
newPasswordHint: "newPasswordHint",
};
});
function setupTdeOffboardingMocks() {
keyService.userKey$.mockReturnValue(of(userKey));
keyService.encryptUserKeyWithMasterKey.mockResolvedValue(masterKeyEncryptedUserKey);
}
it("should successfully set an initial password for the TDE offboarding user", async () => {
// Arrange
setupTdeOffboardingMocks();
const request = new UpdateTdeOffboardingPasswordRequest();
request.key = masterKeyEncryptedUserKey[1].encryptedString;
request.newMasterPasswordHash = credentials.newServerMasterKeyHash;
request.masterPasswordHint = credentials.newPasswordHint;
// Act
await sut.setInitialPasswordTdeOffboarding(credentials, userId);
// Assert
expect(masterPasswordApiService.putUpdateTdeOffboardingPassword).toHaveBeenCalledTimes(1);
expect(masterPasswordApiService.putUpdateTdeOffboardingPassword).toHaveBeenCalledWith(
request,
);
});
describe("given the initial password has been successfully set", () => {
it("should clear the ForceSetPasswordReason by setting it to None", async () => {
// Arrange
setupTdeOffboardingMocks();
// Act
await sut.setInitialPasswordTdeOffboarding(credentials, userId);
// Assert
expect(masterPasswordApiService.putUpdateTdeOffboardingPassword).toHaveBeenCalledTimes(1);
expect(masterPasswordService.setForceSetPasswordReason).toHaveBeenCalledWith(
ForceSetPasswordReason.None,
userId,
);
});
});
describe("general error handling", () => {
["newMasterKey", "newServerMasterKeyHash", "newPasswordHint"].forEach((key) => {
it(`should throw if ${key} is not provided on the SetInitialPasswordTdeOffboardingCredentials object`, async () => {
// Arrange
const invalidCredentials: SetInitialPasswordTdeOffboardingCredentials = {
...credentials,
[key]: null,
};
// Act
const promise = sut.setInitialPasswordTdeOffboarding(invalidCredentials, userId);
// Assert
await expect(promise).rejects.toThrow(`${key} not found. Could not set password.`);
});
});
it(`should throw if the userId was not passed in`, async () => {
// Arrange
userId = null;
// Act
const promise = sut.setInitialPasswordTdeOffboarding(credentials, userId);
// Assert
await expect(promise).rejects.toThrow("userId not found. Could not set password.");
});
it(`should throw if the userKey was not found`, async () => {
// Arrange
keyService.userKey$.mockReturnValue(of(null));
// Act
const promise = sut.setInitialPasswordTdeOffboarding(credentials, userId);
// Assert
await expect(promise).rejects.toThrow("userKey not found. Could not set password.");
});
it(`should throw if a newMasterKeyEncryptedUserKey was not returned`, async () => {
// Arrange
masterKeyEncryptedUserKey[1].encryptedString = "" as EncryptedString;
setupTdeOffboardingMocks();
// Act
const promise = sut.setInitialPasswordTdeOffboarding(credentials, userId);
// Assert
await expect(promise).rejects.toThrow(
"newMasterKeyEncryptedUserKey not found. Could not set password.",
);
});
});
});
}); });

View File

@@ -21,7 +21,12 @@
[userId]="userId" [userId]="userId"
[loading]="submitting" [loading]="submitting"
[masterPasswordPolicyOptions]="masterPasswordPolicyOptions" [masterPasswordPolicyOptions]="masterPasswordPolicyOptions"
[primaryButtonText]="{ key: 'createAccount' }" [primaryButtonText]="{
key:
userType === SetInitialPasswordUserType.OFFBOARDED_TDE_ORG_USER
? 'setPassword'
: 'createAccount',
}"
[secondaryButtonText]="{ key: 'logOut' }" [secondaryButtonText]="{ key: 'logOut' }"
(onPasswordFormSubmit)="handlePasswordFormSubmit($event)" (onPasswordFormSubmit)="handlePasswordFormSubmit($event)"
(onSecondaryButtonClick)="logout()" (onSecondaryButtonClick)="logout()"

View File

@@ -10,14 +10,20 @@ import {
InputPasswordFlow, InputPasswordFlow,
PasswordInputResult, PasswordInputResult,
} from "@bitwarden/auth/angular"; } from "@bitwarden/auth/angular";
// 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 { LogoutService } from "@bitwarden/auth/common";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; 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 { 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 { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.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 { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { assertTruthy, assertNonNullish } from "@bitwarden/common/auth/utils";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; 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 { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import { SyncService } from "@bitwarden/common/platform/sync"; import { SyncService } from "@bitwarden/common/platform/sync";
@@ -33,6 +39,7 @@ import { I18nPipe } from "@bitwarden/ui-common";
import { import {
SetInitialPasswordCredentials, SetInitialPasswordCredentials,
SetInitialPasswordService, SetInitialPasswordService,
SetInitialPasswordTdeOffboardingCredentials,
SetInitialPasswordUserType, SetInitialPasswordUserType,
} from "./set-initial-password.service.abstraction"; } from "./set-initial-password.service.abstraction";
@@ -54,6 +61,7 @@ export class SetInitialPasswordComponent implements OnInit {
protected submitting = false; protected submitting = false;
protected userId?: UserId; protected userId?: UserId;
protected userType?: SetInitialPasswordUserType; protected userType?: SetInitialPasswordUserType;
protected SetInitialPasswordUserType = SetInitialPasswordUserType;
constructor( constructor(
private accountService: AccountService, private accountService: AccountService,
@@ -61,10 +69,13 @@ export class SetInitialPasswordComponent implements OnInit {
private anonLayoutWrapperDataService: AnonLayoutWrapperDataService, private anonLayoutWrapperDataService: AnonLayoutWrapperDataService,
private dialogService: DialogService, private dialogService: DialogService,
private i18nService: I18nService, private i18nService: I18nService,
private logoutService: LogoutService,
private logService: LogService,
private masterPasswordService: InternalMasterPasswordServiceAbstraction, private masterPasswordService: InternalMasterPasswordServiceAbstraction,
private messagingService: MessagingService, private messagingService: MessagingService,
private organizationApiService: OrganizationApiServiceAbstraction, private organizationApiService: OrganizationApiServiceAbstraction,
private policyApiService: PolicyApiServiceAbstraction, private policyApiService: PolicyApiServiceAbstraction,
private policyService: PolicyService,
private router: Router, private router: Router,
private setInitialPasswordService: SetInitialPasswordService, private setInitialPasswordService: SetInitialPasswordService,
private ssoLoginService: SsoLoginServiceAbstraction, private ssoLoginService: SsoLoginServiceAbstraction,
@@ -80,13 +91,13 @@ export class SetInitialPasswordComponent implements OnInit {
this.userId = activeAccount?.id; this.userId = activeAccount?.id;
this.email = activeAccount?.email; this.email = activeAccount?.email;
await this.determineUserType(); await this.establishUserType();
await this.handleQueryParams(); await this.getOrgInfo();
this.initializing = false; this.initializing = false;
} }
private async determineUserType() { private async establishUserType() {
if (!this.userId) { if (!this.userId) {
throw new Error("userId not found. Could not determine user type."); throw new Error("userId not found. Could not determine user type.");
} }
@@ -95,6 +106,14 @@ export class SetInitialPasswordComponent implements OnInit {
this.masterPasswordService.forceSetPasswordReason$(this.userId), this.masterPasswordService.forceSetPasswordReason$(this.userId),
); );
if (this.forceSetPasswordReason === ForceSetPasswordReason.SsoNewJitProvisionedUser) {
this.userType = SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER;
this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({
pageTitle: { key: "joinOrganization" },
pageSubtitle: { key: "finishJoiningThisOrganizationBySettingAMasterPassword" },
});
}
if ( if (
this.forceSetPasswordReason === this.forceSetPasswordReason ===
ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission
@@ -104,20 +123,35 @@ export class SetInitialPasswordComponent implements OnInit {
pageTitle: { key: "setMasterPassword" }, pageTitle: { key: "setMasterPassword" },
pageSubtitle: { key: "orgPermissionsUpdatedMustSetPassword" }, pageSubtitle: { key: "orgPermissionsUpdatedMustSetPassword" },
}); });
} else { }
this.userType = SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER;
if (this.forceSetPasswordReason === ForceSetPasswordReason.TdeOffboarding) {
this.userType = SetInitialPasswordUserType.OFFBOARDED_TDE_ORG_USER;
this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({ this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({
pageTitle: { key: "joinOrganization" }, pageTitle: { key: "setMasterPassword" },
pageSubtitle: { key: "finishJoiningThisOrganizationBySettingAMasterPassword" }, pageSubtitle: { key: "tdeDisabledMasterPasswordRequired" },
}); });
} }
// If we somehow end up here without a reason, navigate to root
if (this.forceSetPasswordReason === ForceSetPasswordReason.None) {
await this.router.navigate(["/"]);
}
} }
private async handleQueryParams() { private async getOrgInfo() {
if (!this.userId) { if (!this.userId) {
throw new Error("userId not found. Could not handle query params."); throw new Error("userId not found. Could not handle query params.");
} }
if (this.userType === SetInitialPasswordUserType.OFFBOARDED_TDE_ORG_USER) {
this.masterPasswordPolicyOptions =
(await firstValueFrom(this.policyService.masterPasswordPolicyOptions$(this.userId))) ??
null;
return;
}
const qParams = await firstValueFrom(this.activatedRoute.queryParams); const qParams = await firstValueFrom(this.activatedRoute.queryParams);
this.orgSsoIdentifier = this.orgSsoIdentifier =
@@ -146,38 +180,34 @@ export class SetInitialPasswordComponent implements OnInit {
protected async handlePasswordFormSubmit(passwordInputResult: PasswordInputResult) { protected async handlePasswordFormSubmit(passwordInputResult: PasswordInputResult) {
this.submitting = true; this.submitting = true;
if (!passwordInputResult.newMasterKey) { switch (this.userType) {
throw new Error("newMasterKey not found. Could not set initial password."); case SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER:
} case SetInitialPasswordUserType.TDE_ORG_USER_RESET_PASSWORD_PERMISSION_REQUIRES_MP:
if (!passwordInputResult.newServerMasterKeyHash) { await this.setInitialPassword(passwordInputResult);
throw new Error("newServerMasterKeyHash not found. Could not set initial password."); break;
} case SetInitialPasswordUserType.OFFBOARDED_TDE_ORG_USER:
if (!passwordInputResult.newLocalMasterKeyHash) { await this.setInitialPasswordTdeOffboarding(passwordInputResult);
throw new Error("newLocalMasterKeyHash not found. Could not set initial password."); break;
} default:
// newPasswordHint can have an empty string as a valid value, so we specifically check for null or undefined this.logService.error(
if (passwordInputResult.newPasswordHint == null) { `Unexpected user type: ${this.userType}. Could not set initial password.`,
throw new Error("newPasswordHint not found. Could not set initial password."); );
} this.validationService.showError("Unexpected user type. Could not set initial password.");
if (!passwordInputResult.kdfConfig) {
throw new Error("kdfConfig not found. Could not set initial password.");
}
if (!this.userId) {
throw new Error("userId not found. Could not set initial password.");
}
if (!this.userType) {
throw new Error("userType not found. Could not set initial password.");
}
if (!this.orgSsoIdentifier) {
throw new Error("orgSsoIdentifier not found. Could not set initial password.");
}
if (!this.orgId) {
throw new Error("orgId not found. Could not set initial password.");
}
// resetPasswordAutoEnroll can have `false` as a valid value, so we specifically check for null or undefined
if (this.resetPasswordAutoEnroll == null) {
throw new Error("resetPasswordAutoEnroll not found. Could not set initial password.");
} }
}
private async setInitialPassword(passwordInputResult: PasswordInputResult) {
const ctx = "Could not set initial password.";
assertTruthy(passwordInputResult.newMasterKey, "newMasterKey", ctx);
assertTruthy(passwordInputResult.newServerMasterKeyHash, "newServerMasterKeyHash", ctx);
assertTruthy(passwordInputResult.newLocalMasterKeyHash, "newLocalMasterKeyHash", ctx);
assertTruthy(passwordInputResult.kdfConfig, "kdfConfig", ctx);
assertTruthy(this.orgSsoIdentifier, "orgSsoIdentifier", ctx);
assertTruthy(this.orgId, "orgId", ctx);
assertTruthy(this.userType, "userType", ctx);
assertTruthy(this.userId, "userId", ctx);
assertNonNullish(passwordInputResult.newPasswordHint, "newPasswordHint", ctx); // can have an empty string as a valid value, so check non-nullish
assertNonNullish(this.resetPasswordAutoEnroll, "resetPasswordAutoEnroll", ctx); // can have `false` as a valid value, so check non-nullish
try { try {
const credentials: SetInitialPasswordCredentials = { const credentials: SetInitialPasswordCredentials = {
@@ -202,11 +232,44 @@ export class SetInitialPasswordComponent implements OnInit {
this.submitting = false; this.submitting = false;
await this.router.navigate(["vault"]); await this.router.navigate(["vault"]);
} catch (e) { } catch (e) {
this.logService.error("Error setting initial password", e);
this.validationService.showError(e); this.validationService.showError(e);
this.submitting = false; this.submitting = false;
} }
} }
private async setInitialPasswordTdeOffboarding(passwordInputResult: PasswordInputResult) {
const ctx = "Could not set initial password.";
assertTruthy(passwordInputResult.newMasterKey, "newMasterKey", ctx);
assertTruthy(passwordInputResult.newServerMasterKeyHash, "newServerMasterKeyHash", ctx);
assertTruthy(this.userId, "userId", ctx);
assertNonNullish(passwordInputResult.newPasswordHint, "newPasswordHint", ctx); // can have an empty string as a valid value, so check non-nullish
try {
const credentials: SetInitialPasswordTdeOffboardingCredentials = {
newMasterKey: passwordInputResult.newMasterKey,
newServerMasterKeyHash: passwordInputResult.newServerMasterKeyHash,
newPasswordHint: passwordInputResult.newPasswordHint,
};
await this.setInitialPasswordService.setInitialPasswordTdeOffboarding(
credentials,
this.userId,
);
this.showSuccessToastByUserType();
await this.logoutService.logout(this.userId);
// navigate to root so redirect guard can properly route next active user or null user to correct page
await this.router.navigate(["/"]);
} catch (e) {
this.logService.error("Error setting initial password during TDE offboarding", e);
this.validationService.showError(e);
} finally {
this.submitting = false;
}
}
private showSuccessToastByUserType() { private showSuccessToastByUserType() {
if (this.userType === SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER) { if (this.userType === SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER) {
this.toastService.showToast({ this.toastService.showToast({
@@ -220,12 +283,7 @@ export class SetInitialPasswordComponent implements OnInit {
title: "", title: "",
message: this.i18nService.t("inviteAccepted"), message: this.i18nService.t("inviteAccepted"),
}); });
} } else {
if (
this.userType ===
SetInitialPasswordUserType.TDE_ORG_USER_RESET_PASSWORD_PERMISSION_REQUIRES_MP
) {
this.toastService.showToast({ this.toastService.showToast({
variant: "success", variant: "success",
title: "", title: "",

View File

@@ -19,6 +19,12 @@ export const _SetInitialPasswordUserType = {
*/ */
TDE_ORG_USER_RESET_PASSWORD_PERMISSION_REQUIRES_MP: TDE_ORG_USER_RESET_PASSWORD_PERMISSION_REQUIRES_MP:
"tde_org_user_reset_password_permission_requires_mp", "tde_org_user_reset_password_permission_requires_mp",
/**
* A user in an org that offboarded from trusted device encryption and is now a
* master-password-encryption org
*/
OFFBOARDED_TDE_ORG_USER: "offboarded_tde_org_user",
} as const; } as const;
type _SetInitialPasswordUserType = typeof _SetInitialPasswordUserType; type _SetInitialPasswordUserType = typeof _SetInitialPasswordUserType;
@@ -40,6 +46,12 @@ export interface SetInitialPasswordCredentials {
resetPasswordAutoEnroll: boolean; resetPasswordAutoEnroll: boolean;
} }
export interface SetInitialPasswordTdeOffboardingCredentials {
newMasterKey: MasterKey;
newServerMasterKeyHash: string;
newPasswordHint: string;
}
/** /**
* Handles setting an initial password for an existing authed user. * Handles setting an initial password for an existing authed user.
* *
@@ -61,4 +73,17 @@ export abstract class SetInitialPasswordService {
userType: SetInitialPasswordUserType, userType: SetInitialPasswordUserType,
userId: UserId, userId: UserId,
) => Promise<void>; ) => 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:
* - {@link SetInitialPasswordUserType.OFFBOARDED_TDE_ORG_USER}
*
* @param passwordInputResult credentials object received from the `InputPasswordComponent`
* @param userId the account `userId`
*/
abstract setInitialPasswordTdeOffboarding: (
credentials: SetInitialPasswordTdeOffboardingCredentials,
userId: UserId,
) => Promise<void>;
} }

View File

@@ -0,0 +1,45 @@
/**
* Asserts that a value is non-nullish (not `null` or `undefined`); throws if value is nullish.
*
* @param val the value to check
* @param name the name of the value to include in the error message
* @param ctx context to optionally append to the error message
* @throws if the value is null or undefined
*
* @example
*
* ```
* // `newPasswordHint` can have an empty string as a valid value, so we check non-nullish
* this.assertNonNullish(
* passwordInputResult.newPasswordHint,
* "newPasswordHint",
* "Could not set initial password."
* );
* // Output error message: "newPasswordHint is null or undefined. Could not set initial password."
* ```
*
* @remarks
*
* If you use this method repeatedly to check several values, it may help to assign any
* additional context (`ctx`) to a variable and pass it in to each call. This prevents the
* call from reformatting vertically via prettier in your text editor, taking up multiple lines.
*
* For example:
* ```
* const ctx = "Could not set initial password.";
*
* this.assertNonNullish(valueOne, "valueOne", ctx);
* this.assertNonNullish(valueTwo, "valueTwo", ctx);
* this.assertNonNullish(valueThree, "valueThree", ctx);
* ```
*/
export function assertNonNullish<T>(
val: T,
name: string,
ctx?: string,
): asserts val is NonNullable<T> {
if (val == null) {
// If context is provided, append it to the error message with a space before it.
throw new Error(`${name} is null or undefined.${ctx ? ` ${ctx}` : ""}`);
}
}

View File

@@ -0,0 +1,46 @@
/**
* Asserts that a value is truthy; throws if value is falsy.
*
* @param val the value to check
* @param name the name of the value to include in the error message
* @param ctx context to optionally append to the error message
* @throws if the value is falsy (`false`, `""`, `0`, `null`, `undefined`, `void`, or `NaN`)
*
* @example
*
* ```
* this.assertTruthy(
* this.organizationId,
* "organizationId",
* "Could not set initial password."
* );
* // Output error message: "organizationId is falsy. Could not set initial password."
* ```
*
* @remarks
*
* If you use this method repeatedly to check several values, it may help to assign any
* additional context (`ctx`) to a variable and pass it in to each call. This prevents the
* call from reformatting vertically via prettier in your text editor, taking up multiple lines.
*
* For example:
* ```
* const ctx = "Could not set initial password.";
*
* this.assertTruthy(valueOne, "valueOne", ctx);
* this.assertTruthy(valueTwo, "valueTwo", ctx);
* this.assertTruthy(valueThree, "valueThree", ctx);
*/
export function assertTruthy<T>(
val: T,
name: string,
ctx?: string,
): asserts val is Exclude<T, false | "" | 0 | null | undefined | void | 0n> {
// Because `NaN` is a value (not a type) of type 'number', that means we cannot add
// it to the list of falsy values in the type assertion. Instead, we check for it
// separately at runtime.
if (!val || (typeof val === "number" && Number.isNaN(val))) {
// If context is provided, append it to the error message with a space before it.
throw new Error(`${name} is falsy.${ctx ? ` ${ctx}` : ""}`);
}
}

View File

@@ -0,0 +1,2 @@
export { assertTruthy } from "./assert-truthy.util";
export { assertNonNullish } from "./assert-non-nullish.util";