mirror of
https://github.com/bitwarden/browser
synced 2025-12-10 13:23:34 +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:
@@ -14,6 +14,7 @@ import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-conso
|
||||
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 { UpdateTdeOffboardingPasswordRequest } from "@bitwarden/common/auth/models/request/update-tde-offboarding-password.request";
|
||||
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 { KeysRequest } from "@bitwarden/common/models/request/keys.request";
|
||||
@@ -28,6 +29,7 @@ import {
|
||||
SetInitialPasswordService,
|
||||
SetInitialPasswordCredentials,
|
||||
SetInitialPasswordUserType,
|
||||
SetInitialPasswordTdeOffboardingCredentials,
|
||||
} from "./set-initial-password.service.abstraction";
|
||||
|
||||
export class DefaultSetInitialPasswordService implements SetInitialPasswordService {
|
||||
@@ -245,4 +247,44 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 { 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 { 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 { KeysRequest } from "@bitwarden/common/models/request/keys.request";
|
||||
@@ -35,6 +36,7 @@ import { DefaultSetInitialPasswordService } from "./default-set-initial-password
|
||||
import {
|
||||
SetInitialPasswordCredentials,
|
||||
SetInitialPasswordService,
|
||||
SetInitialPasswordTdeOffboardingCredentials,
|
||||
SetInitialPasswordUserType,
|
||||
} from "./set-initial-password.service.abstraction";
|
||||
|
||||
@@ -52,6 +54,11 @@ describe("DefaultSetInitialPasswordService", () => {
|
||||
let organizationUserApiService: MockProxy<OrganizationUserApiService>;
|
||||
let userDecryptionOptionsService: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>;
|
||||
|
||||
let userId: UserId;
|
||||
let userKey: UserKey;
|
||||
let userKeyEncString: EncString;
|
||||
let masterKeyEncryptedUserKey: [UserKey, EncString];
|
||||
|
||||
beforeEach(() => {
|
||||
apiService = mock<ApiService>();
|
||||
encryptService = mock<EncryptService>();
|
||||
@@ -64,6 +71,11 @@ describe("DefaultSetInitialPasswordService", () => {
|
||||
organizationUserApiService = mock<OrganizationUserApiService>();
|
||||
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(
|
||||
apiService,
|
||||
encryptService,
|
||||
@@ -86,13 +98,8 @@ describe("DefaultSetInitialPasswordService", () => {
|
||||
// Mock function parameters
|
||||
let credentials: SetInitialPasswordCredentials;
|
||||
let userType: SetInitialPasswordUserType;
|
||||
let userId: UserId;
|
||||
|
||||
// Mock other function data
|
||||
let userKey: UserKey;
|
||||
let userKeyEncString: EncString;
|
||||
let masterKeyEncryptedUserKey: [UserKey, EncString];
|
||||
|
||||
let existingUserPublicKey: UserPublicKey;
|
||||
let existingUserPrivateKey: UserPrivateKey;
|
||||
let userKeyEncryptedPrivateKey: EncString;
|
||||
@@ -121,14 +128,9 @@ describe("DefaultSetInitialPasswordService", () => {
|
||||
orgId: "orgId",
|
||||
resetPasswordAutoEnroll: false,
|
||||
};
|
||||
userId = "userId" as UserId;
|
||||
userType = SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER;
|
||||
|
||||
// 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;
|
||||
existingUserPrivateKey = Utils.fromB64ToArray("existingUserPrivateKey") as UserPrivateKey;
|
||||
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.",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,7 +21,12 @@
|
||||
[userId]="userId"
|
||||
[loading]="submitting"
|
||||
[masterPasswordPolicyOptions]="masterPasswordPolicyOptions"
|
||||
[primaryButtonText]="{ key: 'createAccount' }"
|
||||
[primaryButtonText]="{
|
||||
key:
|
||||
userType === SetInitialPasswordUserType.OFFBOARDED_TDE_ORG_USER
|
||||
? 'setPassword'
|
||||
: 'createAccount',
|
||||
}"
|
||||
[secondaryButtonText]="{ key: 'logOut' }"
|
||||
(onPasswordFormSubmit)="handlePasswordFormSubmit($event)"
|
||||
(onSecondaryButtonClick)="logout()"
|
||||
|
||||
@@ -10,14 +10,20 @@ import {
|
||||
InputPasswordFlow,
|
||||
PasswordInputResult,
|
||||
} 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 { 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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
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 { 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 { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
@@ -33,6 +39,7 @@ import { I18nPipe } from "@bitwarden/ui-common";
|
||||
import {
|
||||
SetInitialPasswordCredentials,
|
||||
SetInitialPasswordService,
|
||||
SetInitialPasswordTdeOffboardingCredentials,
|
||||
SetInitialPasswordUserType,
|
||||
} from "./set-initial-password.service.abstraction";
|
||||
|
||||
@@ -54,6 +61,7 @@ export class SetInitialPasswordComponent implements OnInit {
|
||||
protected submitting = false;
|
||||
protected userId?: UserId;
|
||||
protected userType?: SetInitialPasswordUserType;
|
||||
protected SetInitialPasswordUserType = SetInitialPasswordUserType;
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
@@ -61,10 +69,13 @@ export class SetInitialPasswordComponent implements OnInit {
|
||||
private anonLayoutWrapperDataService: AnonLayoutWrapperDataService,
|
||||
private dialogService: DialogService,
|
||||
private i18nService: I18nService,
|
||||
private logoutService: LogoutService,
|
||||
private logService: LogService,
|
||||
private masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
private messagingService: MessagingService,
|
||||
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||
private policyApiService: PolicyApiServiceAbstraction,
|
||||
private policyService: PolicyService,
|
||||
private router: Router,
|
||||
private setInitialPasswordService: SetInitialPasswordService,
|
||||
private ssoLoginService: SsoLoginServiceAbstraction,
|
||||
@@ -80,13 +91,13 @@ export class SetInitialPasswordComponent implements OnInit {
|
||||
this.userId = activeAccount?.id;
|
||||
this.email = activeAccount?.email;
|
||||
|
||||
await this.determineUserType();
|
||||
await this.handleQueryParams();
|
||||
await this.establishUserType();
|
||||
await this.getOrgInfo();
|
||||
|
||||
this.initializing = false;
|
||||
}
|
||||
|
||||
private async determineUserType() {
|
||||
private async establishUserType() {
|
||||
if (!this.userId) {
|
||||
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),
|
||||
);
|
||||
|
||||
if (this.forceSetPasswordReason === ForceSetPasswordReason.SsoNewJitProvisionedUser) {
|
||||
this.userType = SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER;
|
||||
this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({
|
||||
pageTitle: { key: "joinOrganization" },
|
||||
pageSubtitle: { key: "finishJoiningThisOrganizationBySettingAMasterPassword" },
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
this.forceSetPasswordReason ===
|
||||
ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission
|
||||
@@ -104,20 +123,35 @@ export class SetInitialPasswordComponent implements OnInit {
|
||||
pageTitle: { key: "setMasterPassword" },
|
||||
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({
|
||||
pageTitle: { key: "joinOrganization" },
|
||||
pageSubtitle: { key: "finishJoiningThisOrganizationBySettingAMasterPassword" },
|
||||
pageTitle: { key: "setMasterPassword" },
|
||||
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) {
|
||||
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);
|
||||
|
||||
this.orgSsoIdentifier =
|
||||
@@ -146,38 +180,34 @@ export class SetInitialPasswordComponent implements OnInit {
|
||||
protected async handlePasswordFormSubmit(passwordInputResult: PasswordInputResult) {
|
||||
this.submitting = true;
|
||||
|
||||
if (!passwordInputResult.newMasterKey) {
|
||||
throw new Error("newMasterKey not found. Could not set initial password.");
|
||||
}
|
||||
if (!passwordInputResult.newServerMasterKeyHash) {
|
||||
throw new Error("newServerMasterKeyHash not found. Could not set initial password.");
|
||||
}
|
||||
if (!passwordInputResult.newLocalMasterKeyHash) {
|
||||
throw new Error("newLocalMasterKeyHash not found. Could not set initial password.");
|
||||
}
|
||||
// newPasswordHint can have an empty string as a valid value, so we specifically check for null or undefined
|
||||
if (passwordInputResult.newPasswordHint == null) {
|
||||
throw new Error("newPasswordHint not found. 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.");
|
||||
switch (this.userType) {
|
||||
case SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER:
|
||||
case SetInitialPasswordUserType.TDE_ORG_USER_RESET_PASSWORD_PERMISSION_REQUIRES_MP:
|
||||
await this.setInitialPassword(passwordInputResult);
|
||||
break;
|
||||
case SetInitialPasswordUserType.OFFBOARDED_TDE_ORG_USER:
|
||||
await this.setInitialPasswordTdeOffboarding(passwordInputResult);
|
||||
break;
|
||||
default:
|
||||
this.logService.error(
|
||||
`Unexpected user type: ${this.userType}. Could not set initial password.`,
|
||||
);
|
||||
this.validationService.showError("Unexpected user type. 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 {
|
||||
const credentials: SetInitialPasswordCredentials = {
|
||||
@@ -202,11 +232,44 @@ export class SetInitialPasswordComponent implements OnInit {
|
||||
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);
|
||||
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() {
|
||||
if (this.userType === SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER) {
|
||||
this.toastService.showToast({
|
||||
@@ -220,12 +283,7 @@ export class SetInitialPasswordComponent implements OnInit {
|
||||
title: "",
|
||||
message: this.i18nService.t("inviteAccepted"),
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
this.userType ===
|
||||
SetInitialPasswordUserType.TDE_ORG_USER_RESET_PASSWORD_PERMISSION_REQUIRES_MP
|
||||
) {
|
||||
} else {
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: "",
|
||||
|
||||
@@ -19,6 +19,12 @@ export const _SetInitialPasswordUserType = {
|
||||
*/
|
||||
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;
|
||||
|
||||
type _SetInitialPasswordUserType = typeof _SetInitialPasswordUserType;
|
||||
@@ -40,6 +46,12 @@ export interface SetInitialPasswordCredentials {
|
||||
resetPasswordAutoEnroll: boolean;
|
||||
}
|
||||
|
||||
export interface SetInitialPasswordTdeOffboardingCredentials {
|
||||
newMasterKey: MasterKey;
|
||||
newServerMasterKeyHash: string;
|
||||
newPasswordHint: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles setting an initial password for an existing authed user.
|
||||
*
|
||||
@@ -61,4 +73,17 @@ export abstract class SetInitialPasswordService {
|
||||
userType: SetInitialPasswordUserType,
|
||||
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:
|
||||
* - {@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>;
|
||||
}
|
||||
|
||||
45
libs/common/src/auth/utils/assert-non-nullish.util.ts
Normal file
45
libs/common/src/auth/utils/assert-non-nullish.util.ts
Normal 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}` : ""}`);
|
||||
}
|
||||
}
|
||||
46
libs/common/src/auth/utils/assert-truthy.util.ts
Normal file
46
libs/common/src/auth/utils/assert-truthy.util.ts
Normal 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}` : ""}`);
|
||||
}
|
||||
}
|
||||
2
libs/common/src/auth/utils/index.ts
Normal file
2
libs/common/src/auth/utils/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { assertTruthy } from "./assert-truthy.util";
|
||||
export { assertNonNullish } from "./assert-non-nullish.util";
|
||||
Reference in New Issue
Block a user