1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-04 18:53:20 +00:00

feat(register): [PM-27085] Account Register Uses New Data Types - Added feature flag.

This commit is contained in:
Patrick Pimentel
2026-01-22 16:03:51 -05:00
parent 0d5e304d44
commit 3a44b4b3df
11 changed files with 511 additions and 283 deletions

View File

@@ -9,19 +9,23 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-api.service";
import { RegisterFinishV2Request } from "@bitwarden/common/auth/models/request/registration/register-finish-v2.request";
import { RegisterFinishRequest } from "@bitwarden/common/auth/models/request/registration/register-finish.request";
import { OrganizationInvite } from "@bitwarden/common/auth/services/organization-invite/organization-invite";
import { OrganizationInviteService } from "@bitwarden/common/auth/services/organization-invite/organization-invite.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import {
MasterPasswordUnlockData,
MasterPasswordSalt,
} from "@bitwarden/common/key-management/master-password/types/master-password.types";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { CsprngArray } from "@bitwarden/common/types/csprng";
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
import { DEFAULT_KDF_CONFIG, KeyService } from "@bitwarden/key-management";
import { DEFAULT_KDF_CONFIG, KeyService, KdfType } from "@bitwarden/key-management";
import { WebRegistrationFinishService } from "./web-registration-finish.service";
@@ -35,6 +39,7 @@ describe("WebRegistrationFinishService", () => {
let logService: MockProxy<LogService>;
let policyService: MockProxy<PolicyService>;
let masterPasswordService: MockProxy<MasterPasswordServiceAbstraction>;
let configService: MockProxy<ConfigService>;
beforeEach(() => {
keyService = mock<KeyService>();
@@ -43,13 +48,14 @@ describe("WebRegistrationFinishService", () => {
policyApiService = mock<PolicyApiServiceAbstraction>();
logService = mock<LogService>();
policyService = mock<PolicyService>();
masterPasswordService = mock<MasterPasswordServiceAbstraction>();
configService = mock<ConfigService>();
service = new WebRegistrationFinishService(
keyService,
accountApiService,
masterPasswordService,
configService,
organizationInviteService,
policyApiService,
logService,
@@ -228,226 +234,296 @@ describe("WebRegistrationFinishService", () => {
it("throws an error if the user key cannot be created", async () => {
keyService.makeUserKey.mockResolvedValue([null, null]);
configService.getFeatureFlag.mockResolvedValue(false);
await expect(service.finishRegistration(email, passwordInputResult)).rejects.toThrow(
"User key could not be created",
);
});
it("registers the user when given valid email verification input", async () => {
keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]);
keyService.makeKeyPair.mockResolvedValue(userKeyPair);
accountApiService.registerFinish.mockResolvedValue();
organizationInviteService.getOrganizationInvite.mockResolvedValue(null);
masterPasswordService.emailToSalt.mockReturnValue(salt);
masterPasswordService.makeMasterPasswordAuthenticationData.mockResolvedValue(
masterPasswordAuthentication,
);
masterPasswordService.makeMasterPasswordUnlockData.mockResolvedValue(masterPasswordUnlock);
describe("when feature flag is OFF (old API)", () => {
beforeEach(() => {
configService.getFeatureFlag.mockResolvedValue(false);
});
await service.finishRegistration(email, passwordInputResult, emailVerificationToken);
it("registers the user with KDF fields when given valid email verification input", async () => {
keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]);
keyService.makeKeyPair.mockResolvedValue(userKeyPair);
accountApiService.registerFinish.mockResolvedValue();
organizationInviteService.getOrganizationInvite.mockResolvedValue(null);
expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey);
expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey);
expect(accountApiService.registerFinish).toHaveBeenCalledWith(
expect.objectContaining({
await service.finishRegistration(email, passwordInputResult, emailVerificationToken);
expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey);
expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey);
expect(configService.getFeatureFlag).toHaveBeenCalledWith(
FeatureFlag.PM27044_UpdateRegistrationApis,
);
expect(accountApiService.registerFinish).toHaveBeenCalledWith(
expect.objectContaining({
email,
emailVerificationToken: emailVerificationToken,
masterPasswordHash: passwordInputResult.newServerMasterKeyHash,
masterPasswordHint: passwordInputResult.newPasswordHint,
userSymmetricKey: userKeyEncString.encryptedString,
userAsymmetricKeys: {
publicKey: userKeyPair[0],
encryptedPrivateKey: userKeyPair[1].encryptedString,
},
kdf: KdfType.PBKDF2_SHA256,
kdfIterations: DEFAULT_KDF_CONFIG.iterations,
}),
);
// Verify old API fields are present
const registerCall = accountApiService.registerFinish.mock.calls[0][0];
expect(registerCall).toBeInstanceOf(RegisterFinishRequest);
expect((registerCall as RegisterFinishRequest).kdf).toBeDefined();
expect((registerCall as RegisterFinishRequest).kdfIterations).toBeDefined();
});
it("it registers the user with org invite when given an org invite", async () => {
keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]);
keyService.makeKeyPair.mockResolvedValue(userKeyPair);
accountApiService.registerFinish.mockResolvedValue();
organizationInviteService.getOrganizationInvite.mockResolvedValue(orgInvite);
await service.finishRegistration(email, passwordInputResult);
expect(accountApiService.registerFinish).toHaveBeenCalledWith(
expect.objectContaining({
email,
orgInviteToken: orgInvite.token,
organizationUserId: orgInvite.organizationUserId,
kdf: KdfType.PBKDF2_SHA256,
kdfIterations: DEFAULT_KDF_CONFIG.iterations,
}),
);
});
it("registers the user when given an org sponsored free family plan token", async () => {
keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]);
keyService.makeKeyPair.mockResolvedValue(userKeyPair);
accountApiService.registerFinish.mockResolvedValue();
organizationInviteService.getOrganizationInvite.mockResolvedValue(null);
await service.finishRegistration(
email,
emailVerificationToken: emailVerificationToken,
masterPasswordHash: passwordInputResult.newServerMasterKeyHash,
masterPasswordHint: passwordInputResult.newPasswordHint,
userSymmetricKey: userKeyEncString.encryptedString,
userAsymmetricKeys: {
publicKey: userKeyPair[0],
encryptedPrivateKey: userKeyPair[1].encryptedString,
},
masterPasswordAuthentication: masterPasswordAuthentication,
masterPasswordUnlock: masterPasswordUnlock,
orgInviteToken: undefined,
organizationUserId: undefined,
orgSponsoredFreeFamilyPlanToken: undefined,
acceptEmergencyAccessInviteToken: undefined,
acceptEmergencyAccessId: undefined,
providerInviteToken: undefined,
providerUserId: undefined,
}),
);
passwordInputResult,
undefined,
orgSponsoredFreeFamilyPlanToken,
);
expect(accountApiService.registerFinish).toHaveBeenCalledWith(
expect.objectContaining({
email,
orgSponsoredFreeFamilyPlanToken: orgSponsoredFreeFamilyPlanToken,
}),
);
});
it("registers the user when given an emergency access invite token", async () => {
keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]);
keyService.makeKeyPair.mockResolvedValue(userKeyPair);
accountApiService.registerFinish.mockResolvedValue();
organizationInviteService.getOrganizationInvite.mockResolvedValue(null);
await service.finishRegistration(
email,
passwordInputResult,
undefined,
undefined,
acceptEmergencyAccessInviteToken,
emergencyAccessId,
);
expect(accountApiService.registerFinish).toHaveBeenCalledWith(
expect.objectContaining({
email,
acceptEmergencyAccessInviteToken: acceptEmergencyAccessInviteToken,
acceptEmergencyAccessId: emergencyAccessId,
}),
);
});
it("registers the user when given a provider invite token", async () => {
keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]);
keyService.makeKeyPair.mockResolvedValue(userKeyPair);
accountApiService.registerFinish.mockResolvedValue();
organizationInviteService.getOrganizationInvite.mockResolvedValue(null);
await service.finishRegistration(
email,
passwordInputResult,
undefined,
undefined,
undefined,
undefined,
providerInviteToken,
providerUserId,
);
expect(accountApiService.registerFinish).toHaveBeenCalledWith(
expect.objectContaining({
email,
providerInviteToken: providerInviteToken,
providerUserId: providerUserId,
}),
);
});
});
it("it registers the user when given an org invite", async () => {
keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]);
keyService.makeKeyPair.mockResolvedValue(userKeyPair);
accountApiService.registerFinish.mockResolvedValue();
organizationInviteService.getOrganizationInvite.mockResolvedValue(orgInvite);
masterPasswordService.emailToSalt.mockReturnValue(salt);
masterPasswordService.makeMasterPasswordAuthenticationData.mockResolvedValue(
masterPasswordAuthentication,
);
masterPasswordService.makeMasterPasswordUnlockData.mockResolvedValue(masterPasswordUnlock);
describe("when feature flag is ON (new API)", () => {
beforeEach(() => {
configService.getFeatureFlag.mockResolvedValue(true);
masterPasswordService.emailToSalt.mockReturnValue(salt);
masterPasswordService.makeMasterPasswordAuthenticationData.mockResolvedValue(
masterPasswordAuthentication,
);
masterPasswordService.makeMasterPasswordUnlockData.mockResolvedValue(masterPasswordUnlock);
});
await service.finishRegistration(email, passwordInputResult);
it("registers the user with new data types when given valid email verification input", async () => {
keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]);
keyService.makeKeyPair.mockResolvedValue(userKeyPair);
accountApiService.registerFinish.mockResolvedValue();
organizationInviteService.getOrganizationInvite.mockResolvedValue(null);
expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey);
expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey);
expect(accountApiService.registerFinish).toHaveBeenCalledWith(
expect.objectContaining({
await service.finishRegistration(email, passwordInputResult, emailVerificationToken);
expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey);
expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey);
expect(configService.getFeatureFlag).toHaveBeenCalledWith(
FeatureFlag.PM27044_UpdateRegistrationApis,
);
expect(accountApiService.registerFinish).toHaveBeenCalledWith(
expect.objectContaining({
email,
emailVerificationToken: emailVerificationToken,
masterPasswordHash: passwordInputResult.newServerMasterKeyHash,
masterPasswordHint: passwordInputResult.newPasswordHint,
userSymmetricKey: userKeyEncString.encryptedString,
userAsymmetricKeys: {
publicKey: userKeyPair[0],
encryptedPrivateKey: userKeyPair[1].encryptedString,
},
masterPasswordAuthentication: masterPasswordAuthentication,
masterPasswordUnlock: masterPasswordUnlock,
}),
);
// Verify new API fields are present
const registerCall = accountApiService.registerFinish.mock.calls[0][0];
expect(registerCall).toBeInstanceOf(RegisterFinishV2Request);
expect(
(registerCall as RegisterFinishV2Request).masterPasswordAuthentication,
).toBeDefined();
expect((registerCall as RegisterFinishV2Request).masterPasswordUnlock).toBeDefined();
// Verify old API fields are NOT present
expect((registerCall as any).kdf).toBeUndefined();
expect((registerCall as any).kdfIterations).toBeUndefined();
});
it("it registers the user with org invite when given an org invite", async () => {
keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]);
keyService.makeKeyPair.mockResolvedValue(userKeyPair);
accountApiService.registerFinish.mockResolvedValue();
organizationInviteService.getOrganizationInvite.mockResolvedValue(orgInvite);
await service.finishRegistration(email, passwordInputResult);
expect(accountApiService.registerFinish).toHaveBeenCalledWith(
expect.objectContaining({
email,
orgInviteToken: orgInvite.token,
organizationUserId: orgInvite.organizationUserId,
masterPasswordAuthentication: masterPasswordAuthentication,
masterPasswordUnlock: masterPasswordUnlock,
}),
);
// Verify new API fields are present
const registerCall = accountApiService.registerFinish.mock.calls[0][0];
expect(registerCall).toBeInstanceOf(RegisterFinishV2Request);
});
it("registers the user when given an org sponsored free family plan token", async () => {
keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]);
keyService.makeKeyPair.mockResolvedValue(userKeyPair);
accountApiService.registerFinish.mockResolvedValue();
organizationInviteService.getOrganizationInvite.mockResolvedValue(null);
await service.finishRegistration(
email,
emailVerificationToken: undefined,
masterPasswordHash: passwordInputResult.newServerMasterKeyHash,
masterPasswordHint: passwordInputResult.newPasswordHint,
userSymmetricKey: userKeyEncString.encryptedString,
userAsymmetricKeys: {
publicKey: userKeyPair[0],
encryptedPrivateKey: userKeyPair[1].encryptedString,
},
masterPasswordAuthentication: masterPasswordAuthentication,
masterPasswordUnlock: masterPasswordUnlock,
orgInviteToken: orgInvite.token,
organizationUserId: orgInvite.organizationUserId,
orgSponsoredFreeFamilyPlanToken: undefined,
acceptEmergencyAccessInviteToken: undefined,
acceptEmergencyAccessId: undefined,
providerInviteToken: undefined,
providerUserId: undefined,
}),
);
});
passwordInputResult,
undefined,
orgSponsoredFreeFamilyPlanToken,
);
it("registers the user when given an org sponsored free family plan token", async () => {
keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]);
keyService.makeKeyPair.mockResolvedValue(userKeyPair);
accountApiService.registerFinish.mockResolvedValue();
organizationInviteService.getOrganizationInvite.mockResolvedValue(null);
masterPasswordService.emailToSalt.mockReturnValue(salt);
masterPasswordService.makeMasterPasswordAuthenticationData.mockResolvedValue(
masterPasswordAuthentication,
);
masterPasswordService.makeMasterPasswordUnlockData.mockResolvedValue(masterPasswordUnlock);
expect(accountApiService.registerFinish).toHaveBeenCalledWith(
expect.objectContaining({
email,
orgSponsoredFreeFamilyPlanToken: orgSponsoredFreeFamilyPlanToken,
masterPasswordAuthentication: masterPasswordAuthentication,
masterPasswordUnlock: masterPasswordUnlock,
}),
);
});
await service.finishRegistration(
email,
passwordInputResult,
undefined,
orgSponsoredFreeFamilyPlanToken,
);
it("registers the user when given an emergency access invite token", async () => {
keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]);
keyService.makeKeyPair.mockResolvedValue(userKeyPair);
accountApiService.registerFinish.mockResolvedValue();
organizationInviteService.getOrganizationInvite.mockResolvedValue(null);
expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey);
expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey);
expect(accountApiService.registerFinish).toHaveBeenCalledWith(
expect.objectContaining({
await service.finishRegistration(
email,
emailVerificationToken: undefined,
masterPasswordHash: passwordInputResult.newServerMasterKeyHash,
masterPasswordHint: passwordInputResult.newPasswordHint,
userSymmetricKey: userKeyEncString.encryptedString,
userAsymmetricKeys: {
publicKey: userKeyPair[0],
encryptedPrivateKey: userKeyPair[1].encryptedString,
},
masterPasswordAuthentication: masterPasswordAuthentication,
masterPasswordUnlock: masterPasswordUnlock,
orgInviteToken: undefined,
organizationUserId: undefined,
orgSponsoredFreeFamilyPlanToken: orgSponsoredFreeFamilyPlanToken,
acceptEmergencyAccessInviteToken: undefined,
acceptEmergencyAccessId: undefined,
providerInviteToken: undefined,
providerUserId: undefined,
}),
);
});
passwordInputResult,
undefined,
undefined,
acceptEmergencyAccessInviteToken,
emergencyAccessId,
);
it("registers the user when given an emergency access invite token", async () => {
keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]);
keyService.makeKeyPair.mockResolvedValue(userKeyPair);
accountApiService.registerFinish.mockResolvedValue();
organizationInviteService.getOrganizationInvite.mockResolvedValue(null);
masterPasswordService.emailToSalt.mockReturnValue(salt);
masterPasswordService.makeMasterPasswordAuthenticationData.mockResolvedValue(
masterPasswordAuthentication,
);
masterPasswordService.makeMasterPasswordUnlockData.mockResolvedValue(masterPasswordUnlock);
expect(accountApiService.registerFinish).toHaveBeenCalledWith(
expect.objectContaining({
email,
acceptEmergencyAccessInviteToken: acceptEmergencyAccessInviteToken,
acceptEmergencyAccessId: emergencyAccessId,
masterPasswordAuthentication: masterPasswordAuthentication,
masterPasswordUnlock: masterPasswordUnlock,
}),
);
});
await service.finishRegistration(
email,
passwordInputResult,
undefined,
undefined,
acceptEmergencyAccessInviteToken,
emergencyAccessId,
);
it("registers the user when given a provider invite token", async () => {
keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]);
keyService.makeKeyPair.mockResolvedValue(userKeyPair);
accountApiService.registerFinish.mockResolvedValue();
organizationInviteService.getOrganizationInvite.mockResolvedValue(null);
expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey);
expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey);
expect(accountApiService.registerFinish).toHaveBeenCalledWith(
expect.objectContaining({
await service.finishRegistration(
email,
emailVerificationToken: undefined,
masterPasswordHash: passwordInputResult.newServerMasterKeyHash,
masterPasswordHint: passwordInputResult.newPasswordHint,
userSymmetricKey: userKeyEncString.encryptedString,
userAsymmetricKeys: {
publicKey: userKeyPair[0],
encryptedPrivateKey: userKeyPair[1].encryptedString,
},
masterPasswordAuthentication: masterPasswordAuthentication,
masterPasswordUnlock: masterPasswordUnlock,
orgInviteToken: undefined,
organizationUserId: undefined,
orgSponsoredFreeFamilyPlanToken: undefined,
acceptEmergencyAccessInviteToken: acceptEmergencyAccessInviteToken,
acceptEmergencyAccessId: emergencyAccessId,
providerInviteToken: undefined,
providerUserId: undefined,
}),
);
});
passwordInputResult,
undefined,
undefined,
undefined,
undefined,
providerInviteToken,
providerUserId,
);
it("registers the user when given a provider invite token", async () => {
keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]);
keyService.makeKeyPair.mockResolvedValue(userKeyPair);
accountApiService.registerFinish.mockResolvedValue();
organizationInviteService.getOrganizationInvite.mockResolvedValue(null);
masterPasswordService.emailToSalt.mockReturnValue(salt);
masterPasswordService.makeMasterPasswordAuthenticationData.mockResolvedValue(
masterPasswordAuthentication,
);
masterPasswordService.makeMasterPasswordUnlockData.mockResolvedValue(masterPasswordUnlock);
await service.finishRegistration(
email,
passwordInputResult,
undefined,
undefined,
undefined,
undefined,
providerInviteToken,
providerUserId,
);
expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey);
expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey);
expect(accountApiService.registerFinish).toHaveBeenCalledWith(
expect.objectContaining({
email,
emailVerificationToken: undefined,
masterPasswordHash: passwordInputResult.newServerMasterKeyHash,
masterPasswordHint: passwordInputResult.newPasswordHint,
userSymmetricKey: userKeyEncString.encryptedString,
userAsymmetricKeys: {
publicKey: userKeyPair[0],
encryptedPrivateKey: userKeyPair[1].encryptedString,
},
masterPasswordAuthentication: masterPasswordAuthentication,
masterPasswordUnlock: masterPasswordUnlock,
orgInviteToken: undefined,
organizationUserId: undefined,
orgSponsoredFreeFamilyPlanToken: undefined,
acceptEmergencyAccessInviteToken: undefined,
acceptEmergencyAccessId: undefined,
providerInviteToken: providerInviteToken,
providerUserId: providerUserId,
}),
);
expect(accountApiService.registerFinish).toHaveBeenCalledWith(
expect.objectContaining({
email,
providerInviteToken: providerInviteToken,
providerUserId: providerUserId,
masterPasswordAuthentication: masterPasswordAuthentication,
masterPasswordUnlock: masterPasswordUnlock,
}),
);
});
});
});
});

View File

@@ -12,6 +12,7 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-api.service";
import { RegisterFinishV2Request } from "@bitwarden/common/auth/models/request/registration/register-finish-v2.request";
import { RegisterFinishRequest } from "@bitwarden/common/auth/models/request/registration/register-finish.request";
import { OrganizationInviteService } from "@bitwarden/common/auth/services/organization-invite/organization-invite.service";
import {
@@ -19,6 +20,7 @@ import {
EncString,
} from "@bitwarden/common/key-management/crypto/models/enc-string";
import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { UserKey } from "@bitwarden/common/types/key";
import { KeyService } from "@bitwarden/key-management";
@@ -31,12 +33,13 @@ export class WebRegistrationFinishService
protected keyService: KeyService,
protected accountApiService: AccountApiService,
protected masterPasswordService: MasterPasswordServiceAbstraction,
protected configService: ConfigService,
private organizationInviteService: OrganizationInviteService,
private policyApiService: PolicyApiServiceAbstraction,
private logService: LogService,
private policyService: PolicyService,
) {
super(keyService, accountApiService, masterPasswordService);
super(keyService, accountApiService, masterPasswordService, configService);
}
override async getOrgNameFromOrgInvite(): Promise<string | null> {
@@ -92,7 +95,7 @@ export class WebRegistrationFinishService
emergencyAccessId?: string,
providerInviteToken?: string,
providerUserId?: string,
): Promise<RegisterFinishRequest> {
): Promise<RegisterFinishRequest | RegisterFinishV2Request> {
const registerRequest = await super.buildRegisterRequest(
newUserKey,
email,

View File

@@ -290,6 +290,7 @@ const safeProviders: SafeProvider[] = [
KeyServiceAbstraction,
AccountApiServiceAbstraction,
MasterPasswordServiceAbstraction,
ConfigService,
OrganizationInviteService,
PolicyApiServiceAbstraction,
LogService,

View File

@@ -1598,7 +1598,12 @@ const safeProviders: SafeProvider[] = [
safeProvider({
provide: RegistrationFinishServiceAbstraction,
useClass: DefaultRegistrationFinishService,
deps: [KeyService, AccountApiServiceAbstraction, MasterPasswordServiceAbstraction],
deps: [
KeyService,
AccountApiServiceAbstraction,
MasterPasswordServiceAbstraction,
ConfigService,
],
}),
safeProvider({
provide: TwoFactorAuthComponentService,

View File

@@ -1,6 +1,9 @@
import { MockProxy, mock } from "jest-mock-extended";
import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-api.service";
import { RegisterFinishV2Request } from "@bitwarden/common/auth/models/request/registration/register-finish-v2.request";
import { RegisterFinishRequest } from "@bitwarden/common/auth/models/request/registration/register-finish.request";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import {
@@ -10,10 +13,11 @@ import {
MasterPasswordSalt,
MasterKeyWrappedUserKey,
} from "@bitwarden/common/key-management/master-password/types/master-password.types";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { CsprngArray } from "@bitwarden/common/types/csprng";
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
import { DEFAULT_KDF_CONFIG, KeyService } from "@bitwarden/key-management";
import { DEFAULT_KDF_CONFIG, KeyService, KdfType } from "@bitwarden/key-management";
import { PasswordInputResult } from "../../input-password/password-input-result";
@@ -25,16 +29,19 @@ describe("DefaultRegistrationFinishService", () => {
let keyService: MockProxy<KeyService>;
let accountApiService: MockProxy<AccountApiService>;
let masterPasswordService: MockProxy<MasterPasswordServiceAbstraction>;
let configService: MockProxy<ConfigService>;
beforeEach(() => {
keyService = mock<KeyService>();
accountApiService = mock<AccountApiService>();
masterPasswordService = mock<MasterPasswordServiceAbstraction>();
configService = mock<ConfigService>();
service = new DefaultRegistrationFinishService(
keyService,
accountApiService,
masterPasswordService,
configService,
);
});
@@ -89,60 +96,126 @@ describe("DefaultRegistrationFinishService", () => {
it("throws an error if the user key cannot be created", async () => {
keyService.makeUserKey.mockResolvedValue([null, null]);
masterPasswordService.emailToSalt.mockReturnValue("salt" as MasterPasswordSalt);
configService.getFeatureFlag.mockResolvedValue(false);
await expect(service.finishRegistration(email, passwordInputResult)).rejects.toThrow(
"User key could not be created",
);
});
it("registers the user when given valid email verification input", async () => {
keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]);
keyService.makeKeyPair.mockResolvedValue(userKeyPair);
accountApiService.registerFinish.mockResolvedValue();
const salt = "salt" as MasterPasswordSalt;
const masterPasswordAuthentication: MasterPasswordAuthenticationData = {
salt,
kdf: DEFAULT_KDF_CONFIG,
masterPasswordAuthenticationHash: "authHash" as MasterPasswordAuthenticationHash,
};
const masterPasswordUnlock = new MasterPasswordUnlockData(
salt,
DEFAULT_KDF_CONFIG,
"wrappedUserKey" as MasterKeyWrappedUserKey,
);
masterPasswordService.emailToSalt.mockReturnValue(salt);
masterPasswordService.makeMasterPasswordAuthenticationData.mockResolvedValue(
masterPasswordAuthentication,
);
masterPasswordService.makeMasterPasswordUnlockData.mockResolvedValue(masterPasswordUnlock);
describe("when feature flag is OFF (old API)", () => {
beforeEach(() => {
configService.getFeatureFlag.mockResolvedValue(false);
});
await service.finishRegistration(email, passwordInputResult, emailVerificationToken);
it("registers the user with KDF fields when given valid email verification input", async () => {
keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]);
keyService.makeKeyPair.mockResolvedValue(userKeyPair);
accountApiService.registerFinish.mockResolvedValue();
expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey);
expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey);
expect(accountApiService.registerFinish).toHaveBeenCalledWith(
expect.objectContaining({
email,
emailVerificationToken: emailVerificationToken,
masterPasswordHash: passwordInputResult.newServerMasterKeyHash,
masterPasswordHint: passwordInputResult.newPasswordHint,
userSymmetricKey: userKeyEncString.encryptedString,
userAsymmetricKeys: {
publicKey: userKeyPair[0],
encryptedPrivateKey: userKeyPair[1].encryptedString,
},
masterPasswordAuthentication: masterPasswordAuthentication,
masterPasswordUnlock: masterPasswordUnlock,
// Web only fields should be undefined
orgInviteToken: undefined,
organizationUserId: undefined,
orgSponsoredFreeFamilyPlanToken: undefined,
acceptEmergencyAccessInviteToken: undefined,
acceptEmergencyAccessId: undefined,
providerInviteToken: undefined,
providerUserId: undefined,
}),
);
await service.finishRegistration(email, passwordInputResult, emailVerificationToken);
expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey);
expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey);
expect(configService.getFeatureFlag).toHaveBeenCalledWith(
FeatureFlag.PM27044_UpdateRegistrationApis,
);
expect(accountApiService.registerFinish).toHaveBeenCalledWith(
expect.objectContaining({
email,
emailVerificationToken: emailVerificationToken,
masterPasswordHash: passwordInputResult.newServerMasterKeyHash,
masterPasswordHint: passwordInputResult.newPasswordHint,
userSymmetricKey: userKeyEncString.encryptedString,
userAsymmetricKeys: {
publicKey: userKeyPair[0],
encryptedPrivateKey: userKeyPair[1].encryptedString,
},
kdf: KdfType.PBKDF2_SHA256,
kdfIterations: DEFAULT_KDF_CONFIG.iterations,
kdfMemory: undefined,
kdfParallelism: undefined,
}),
);
// Verify old API fields are present
const registerCall = accountApiService.registerFinish.mock.calls[0][0];
expect(registerCall).toBeInstanceOf(RegisterFinishRequest);
expect((registerCall as RegisterFinishRequest).kdf).toBeDefined();
expect((registerCall as RegisterFinishRequest).kdfIterations).toBeDefined();
// Verify new API fields are NOT present
expect((registerCall as any).masterPasswordAuthentication).toBeUndefined();
expect((registerCall as any).masterPasswordUnlock).toBeUndefined();
});
});
describe("when feature flag is ON (new API)", () => {
let salt: MasterPasswordSalt;
let masterPasswordAuthentication: MasterPasswordAuthenticationData;
let masterPasswordUnlock: MasterPasswordUnlockData;
beforeEach(() => {
configService.getFeatureFlag.mockResolvedValue(true);
salt = "salt" as MasterPasswordSalt;
masterPasswordAuthentication = {
salt,
kdf: DEFAULT_KDF_CONFIG,
masterPasswordAuthenticationHash: "authHash" as MasterPasswordAuthenticationHash,
};
masterPasswordUnlock = new MasterPasswordUnlockData(
salt,
DEFAULT_KDF_CONFIG,
"wrappedUserKey" as MasterKeyWrappedUserKey,
);
masterPasswordService.emailToSalt.mockReturnValue(salt);
masterPasswordService.makeMasterPasswordAuthenticationData.mockResolvedValue(
masterPasswordAuthentication,
);
masterPasswordService.makeMasterPasswordUnlockData.mockResolvedValue(masterPasswordUnlock);
});
it("registers the user with new data types when given valid email verification input", async () => {
keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]);
keyService.makeKeyPair.mockResolvedValue(userKeyPair);
accountApiService.registerFinish.mockResolvedValue();
await service.finishRegistration(email, passwordInputResult, emailVerificationToken);
expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey);
expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey);
expect(configService.getFeatureFlag).toHaveBeenCalledWith(
FeatureFlag.PM27044_UpdateRegistrationApis,
);
expect(accountApiService.registerFinish).toHaveBeenCalledWith(
expect.objectContaining({
email,
emailVerificationToken: emailVerificationToken,
masterPasswordHash: passwordInputResult.newServerMasterKeyHash,
masterPasswordHint: passwordInputResult.newPasswordHint,
userSymmetricKey: userKeyEncString.encryptedString,
userAsymmetricKeys: {
publicKey: userKeyPair[0],
encryptedPrivateKey: userKeyPair[1].encryptedString,
},
masterPasswordAuthentication: masterPasswordAuthentication,
masterPasswordUnlock: masterPasswordUnlock,
}),
);
// Verify new API fields are present
const registerCall = accountApiService.registerFinish.mock.calls[0][0];
expect(registerCall).toBeInstanceOf(RegisterFinishV2Request);
expect(
(registerCall as RegisterFinishV2Request).masterPasswordAuthentication,
).toBeDefined();
expect((registerCall as RegisterFinishV2Request).masterPasswordUnlock).toBeDefined();
// Verify old API fields are NOT present
expect((registerCall as any).kdf).toBeUndefined();
expect((registerCall as any).kdfIterations).toBeUndefined();
});
});
});
});

View File

@@ -2,15 +2,18 @@
// @ts-strict-ignore
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-api.service";
import { RegisterFinishV2Request } from "@bitwarden/common/auth/models/request/registration/register-finish-v2.request";
import { RegisterFinishRequest } from "@bitwarden/common/auth/models/request/registration/register-finish.request";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import {
EncryptedString,
EncString,
} from "@bitwarden/common/key-management/crypto/models/enc-string";
import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { UserKey } from "@bitwarden/common/types/key";
import { KeyService } from "@bitwarden/key-management";
import { Argon2KdfConfig, KeyService, KdfType } from "@bitwarden/key-management";
import { PasswordInputResult } from "../../input-password/password-input-result";
@@ -21,6 +24,7 @@ export class DefaultRegistrationFinishService implements RegistrationFinishServi
protected keyService: KeyService,
protected accountApiService: AccountApiService,
protected masterPasswordService: MasterPasswordServiceAbstraction,
protected configService: ConfigService,
) {}
getOrgNameFromOrgInvite(): Promise<string | null> {
@@ -79,44 +83,72 @@ export class DefaultRegistrationFinishService implements RegistrationFinishServi
emergencyAccessId?: string, // web only
providerInviteToken?: string, // web only
providerUserId?: string, // web only
): Promise<RegisterFinishRequest> {
): Promise<RegisterFinishRequest | RegisterFinishV2Request> {
const userAsymmetricKeysRequest = new KeysRequest(
userAsymmetricKeys[0],
userAsymmetricKeys[1].encryptedString,
);
// Get salt value, for now we derive it from the email but this could change to be random bytes
// in the future once the email and salt are separated.
const salt = this.masterPasswordService.emailToSalt(email);
const useNewApi = await this.configService.getFeatureFlag(
FeatureFlag.PM27044_UpdateRegistrationApis,
);
const masterPasswordAuthentication =
await this.masterPasswordService.makeMasterPasswordAuthenticationData(
if (useNewApi) {
// New API path - use V2 request with new data types
const salt = this.masterPasswordService.emailToSalt(email);
const masterPasswordAuthentication =
await this.masterPasswordService.makeMasterPasswordAuthenticationData(
passwordInputResult.newPassword,
passwordInputResult.kdfConfig,
salt,
);
const masterPasswordUnlock = await this.masterPasswordService.makeMasterPasswordUnlockData(
passwordInputResult.newPassword,
passwordInputResult.kdfConfig,
salt,
newUserKey,
);
const masterPasswordUnlock = await this.masterPasswordService.makeMasterPasswordUnlockData(
passwordInputResult.newPassword,
passwordInputResult.kdfConfig,
salt,
newUserKey,
);
const registerFinishRequest = new RegisterFinishV2Request(
email,
passwordInputResult.newServerMasterKeyHash,
passwordInputResult.newPasswordHint,
encryptedUserKey,
userAsymmetricKeysRequest,
masterPasswordAuthentication,
masterPasswordUnlock,
);
const registerFinishRequest = new RegisterFinishRequest(
email,
passwordInputResult.newServerMasterKeyHash,
passwordInputResult.newPasswordHint,
encryptedUserKey,
userAsymmetricKeysRequest,
masterPasswordAuthentication,
masterPasswordUnlock,
);
if (emailVerificationToken) {
registerFinishRequest.emailVerificationToken = emailVerificationToken;
}
if (emailVerificationToken) {
registerFinishRequest.emailVerificationToken = emailVerificationToken;
return registerFinishRequest;
} else {
// Old API path - use original request with KDF fields
const kdfConfig = passwordInputResult.kdfConfig;
const registerFinishRequest = new RegisterFinishRequest(
email,
passwordInputResult.newServerMasterKeyHash,
passwordInputResult.newPasswordHint,
encryptedUserKey,
userAsymmetricKeysRequest,
kdfConfig.kdfType,
kdfConfig.iterations,
kdfConfig.kdfType === KdfType.Argon2id ? (kdfConfig as Argon2KdfConfig).memory : undefined,
kdfConfig.kdfType === KdfType.Argon2id
? (kdfConfig as Argon2KdfConfig).parallelism
: undefined,
);
if (emailVerificationToken) {
registerFinishRequest.emailVerificationToken = emailVerificationToken;
}
return registerFinishRequest;
}
return registerFinishRequest;
}
}

View File

@@ -1,3 +1,4 @@
import { RegisterFinishV2Request } from "../models/request/registration/register-finish-v2.request";
import { RegisterFinishRequest } from "../models/request/registration/register-finish.request";
import { RegisterSendVerificationEmailRequest } from "../models/request/registration/register-send-verification-email.request";
import { RegisterVerificationEmailClickedRequest } from "../models/request/registration/register-verification-email-clicked.request";
@@ -49,7 +50,7 @@ export abstract class AccountApiService {
* with the KDF information used during the process.
* @returns A promise that resolves when the registration process is successfully completed.
*/
abstract registerFinish(request: RegisterFinishRequest): Promise<void>;
abstract registerFinish(request: RegisterFinishRequest | RegisterFinishV2Request): Promise<void>;
/**
* Sets the [dbo].[User].[VerifyDevices] flag to true or false.

View File

@@ -0,0 +1,32 @@
import { EncryptedString } from "../../../../key-management/crypto/models/enc-string";
import {
MasterPasswordAuthenticationData,
MasterPasswordUnlockData,
} from "../../../../key-management/master-password/types/master-password.types";
import { KeysRequest } from "../../../../models/request/keys.request";
export class RegisterFinishV2Request {
constructor(
public email: string,
public masterPasswordHash: string,
public masterPasswordHint: string,
public userSymmetricKey: EncryptedString,
public userAsymmetricKeys: KeysRequest,
public masterPasswordAuthentication: MasterPasswordAuthenticationData,
public masterPasswordUnlock: MasterPasswordUnlockData,
public emailVerificationToken?: string,
public orgSponsoredFreeFamilyPlanToken?: string,
public acceptEmergencyAccessInviteToken?: string,
public acceptEmergencyAccessId?: string,
public providerInviteToken?: string,
public providerUserId?: string,
// Org Invite data (only applies on web)
public organizationUserId?: string,
public orgInviteToken?: string,
) {}
}

View File

@@ -1,8 +1,8 @@
// 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 { KdfType } from "@bitwarden/key-management";
import { EncryptedString } from "../../../../key-management/crypto/models/enc-string";
import {
MasterPasswordAuthenticationData,
MasterPasswordUnlockData,
} from "../../../../key-management/master-password/types/master-password.types";
import { KeysRequest } from "../../../../models/request/keys.request";
export class RegisterFinishRequest {
@@ -15,8 +15,10 @@ export class RegisterFinishRequest {
public userSymmetricKey: EncryptedString,
public userAsymmetricKeys: KeysRequest,
public masterPasswordAuthentication: MasterPasswordAuthenticationData,
public masterPasswordUnlock: MasterPasswordUnlockData,
public kdf: KdfType,
public kdfIterations: number,
public kdfMemory?: number,
public kdfParallelism?: number,
public emailVerificationToken?: string,
public orgSponsoredFreeFamilyPlanToken?: string,

View File

@@ -7,6 +7,7 @@ import { LogService } from "../../platform/abstractions/log.service";
import { AccountApiService } from "../abstractions/account-api.service";
import { InternalAccountService } from "../abstractions/account.service";
import { UserVerificationService } from "../abstractions/user-verification/user-verification.service.abstraction";
import { RegisterFinishV2Request } from "../models/request/registration/register-finish-v2.request";
import { RegisterFinishRequest } from "../models/request/registration/register-finish.request";
import { RegisterSendVerificationEmailRequest } from "../models/request/registration/register-send-verification-email.request";
import { RegisterVerificationEmailClickedRequest } from "../models/request/registration/register-verification-email-clicked.request";
@@ -84,7 +85,7 @@ export class AccountApiServiceImplementation implements AccountApiService {
}
}
async registerFinish(request: RegisterFinishRequest): Promise<void> {
async registerFinish(request: RegisterFinishRequest | RegisterFinishV2Request): Promise<void> {
const env = await firstValueFrom(this.environmentService.environment$);
try {

View File

@@ -46,6 +46,7 @@ export enum FeatureFlag {
ConsolidatedSessionTimeoutComponent = "pm-26056-consolidated-session-timeout-component",
PM27279_V2RegistrationTdeJit = "pm-27279-v2-registration-tde-jit",
EnableAccountEncryptionV2KeyConnectorRegistration = "enable-account-encryption-v2-key-connector-registration",
PM27044_UpdateRegistrationApis = "pm-27044-update-registration-apis",
/* Tools */
UseSdkPasswordGenerators = "pm-19976-use-sdk-password-generators",
@@ -153,6 +154,7 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.ConsolidatedSessionTimeoutComponent]: FALSE,
[FeatureFlag.PM27279_V2RegistrationTdeJit]: FALSE,
[FeatureFlag.EnableAccountEncryptionV2KeyConnectorRegistration]: FALSE,
[FeatureFlag.PM27044_UpdateRegistrationApis]: FALSE,
/* Platform */
[FeatureFlag.IpcChannelFramework]: FALSE,