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:
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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()"
|
||||||
|
|||||||
@@ -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: "",
|
||||||
|
|||||||
@@ -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>;
|
||||||
}
|
}
|
||||||
|
|||||||
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