From 1e3072492ac030eb1931b16c6c0420c7734c99de Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Tue, 14 Apr 2026 12:33:58 -0700 Subject: [PATCH 1/5] remove from RegistrationFinishService and update related files --- ...b-registration-finish.service.spec.ts.snap | 301 ------------- .../web-registration-finish.service.spec.ts | 411 ++++++------------ .../web-registration-finish.service.ts | 14 +- apps/web/src/app/core/core.module.ts | 1 - .../src/services/jslib-services.module.ts | 7 +- ...t-registration-finish.service.spec.ts.snap | 61 --- ...efault-registration-finish.service.spec.ts | 208 +++------ .../default-registration-finish.service.ts | 134 ++---- .../auth/abstractions/account-api.service.ts | 13 +- .../registration/register-finish.request.ts | 34 -- .../src/auth/services/account-api.service.ts | 5 +- 11 files changed, 243 insertions(+), 946 deletions(-) delete mode 100644 apps/web/src/app/auth/core/services/registration/__snapshots__/web-registration-finish.service.spec.ts.snap delete mode 100644 libs/auth/src/angular/registration/registration-finish/__snapshots__/default-registration-finish.service.spec.ts.snap delete mode 100644 libs/common/src/auth/models/request/registration/register-finish.request.ts diff --git a/apps/web/src/app/auth/core/services/registration/__snapshots__/web-registration-finish.service.spec.ts.snap b/apps/web/src/app/auth/core/services/registration/__snapshots__/web-registration-finish.service.spec.ts.snap deleted file mode 100644 index fafd44efe68c..000000000000 --- a/apps/web/src/app/auth/core/services/registration/__snapshots__/web-registration-finish.service.spec.ts.snap +++ /dev/null @@ -1,301 +0,0 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing - -exports[`WebRegistrationFinishService finishRegistration() when feature flag is OFF (old API) it registers the user with org invite when given an org invite 1`] = ` -RegisterFinishRequest { - "acceptEmergencyAccessId": undefined, - "acceptEmergencyAccessInviteToken": undefined, - "email": "test@email.com", - "emailVerificationToken": undefined, - "kdf": 0, - "kdfIterations": 600000, - "kdfMemory": undefined, - "kdfParallelism": undefined, - "masterPasswordHash": "newServerMasterKeyHash", - "masterPasswordHint": "newPasswordHint", - "orgInviteToken": "orgInviteToken", - "orgSponsoredFreeFamilyPlanToken": undefined, - "organizationUserId": "organizationUserId", - "providerInviteToken": undefined, - "providerUserId": undefined, - "userAsymmetricKeys": KeysRequest { - "encryptedPrivateKey": "privateKey", - "publicKey": "publicKey", - }, - "userSymmetricKey": "userKeyEncrypted", -} -`; - -exports[`WebRegistrationFinishService finishRegistration() when feature flag is OFF (old API) registers the user when given a provider invite token 1`] = ` -RegisterFinishRequest { - "acceptEmergencyAccessId": undefined, - "acceptEmergencyAccessInviteToken": undefined, - "email": "test@email.com", - "emailVerificationToken": undefined, - "kdf": 0, - "kdfIterations": 600000, - "kdfMemory": undefined, - "kdfParallelism": undefined, - "masterPasswordHash": "newServerMasterKeyHash", - "masterPasswordHint": "newPasswordHint", - "orgInviteToken": undefined, - "orgSponsoredFreeFamilyPlanToken": undefined, - "organizationUserId": undefined, - "providerInviteToken": "providerInviteToken", - "providerUserId": "providerUserId", - "userAsymmetricKeys": KeysRequest { - "encryptedPrivateKey": "privateKey", - "publicKey": "publicKey", - }, - "userSymmetricKey": "userKeyEncrypted", -} -`; - -exports[`WebRegistrationFinishService finishRegistration() when feature flag is OFF (old API) registers the user when given an emergency access invite token 1`] = ` -RegisterFinishRequest { - "acceptEmergencyAccessId": "emergencyAccessId", - "acceptEmergencyAccessInviteToken": "acceptEmergencyAccessInviteToken", - "email": "test@email.com", - "emailVerificationToken": undefined, - "kdf": 0, - "kdfIterations": 600000, - "kdfMemory": undefined, - "kdfParallelism": undefined, - "masterPasswordHash": "newServerMasterKeyHash", - "masterPasswordHint": "newPasswordHint", - "orgInviteToken": undefined, - "orgSponsoredFreeFamilyPlanToken": undefined, - "organizationUserId": undefined, - "providerInviteToken": undefined, - "providerUserId": undefined, - "userAsymmetricKeys": KeysRequest { - "encryptedPrivateKey": "privateKey", - "publicKey": "publicKey", - }, - "userSymmetricKey": "userKeyEncrypted", -} -`; - -exports[`WebRegistrationFinishService finishRegistration() when feature flag is OFF (old API) registers the user when given an org sponsored free family plan token 1`] = ` -RegisterFinishRequest { - "acceptEmergencyAccessId": undefined, - "acceptEmergencyAccessInviteToken": undefined, - "email": "test@email.com", - "emailVerificationToken": undefined, - "kdf": 0, - "kdfIterations": 600000, - "kdfMemory": undefined, - "kdfParallelism": undefined, - "masterPasswordHash": "newServerMasterKeyHash", - "masterPasswordHint": "newPasswordHint", - "orgInviteToken": undefined, - "orgSponsoredFreeFamilyPlanToken": "orgSponsoredFreeFamilyPlanToken", - "organizationUserId": undefined, - "providerInviteToken": undefined, - "providerUserId": undefined, - "userAsymmetricKeys": KeysRequest { - "encryptedPrivateKey": "privateKey", - "publicKey": "publicKey", - }, - "userSymmetricKey": "userKeyEncrypted", -} -`; - -exports[`WebRegistrationFinishService finishRegistration() when feature flag is OFF (old API) registers the user with KDF fields when given valid email verification input 1`] = ` -RegisterFinishRequest { - "acceptEmergencyAccessId": undefined, - "acceptEmergencyAccessInviteToken": undefined, - "email": "test@email.com", - "emailVerificationToken": "emailVerificationToken", - "kdf": 0, - "kdfIterations": 600000, - "kdfMemory": undefined, - "kdfParallelism": undefined, - "masterPasswordHash": "newServerMasterKeyHash", - "masterPasswordHint": "newPasswordHint", - "orgInviteToken": undefined, - "orgSponsoredFreeFamilyPlanToken": undefined, - "organizationUserId": undefined, - "providerInviteToken": undefined, - "providerUserId": undefined, - "userAsymmetricKeys": KeysRequest { - "encryptedPrivateKey": "privateKey", - "publicKey": "publicKey", - }, - "userSymmetricKey": "userKeyEncrypted", -} -`; - -exports[`WebRegistrationFinishService finishRegistration() when feature flag is ON (new API) derives the master key and registers the user with new data types 1`] = ` -RegisterFinishRequestWithAuthUnlockDataTypes { - "acceptEmergencyAccessId": undefined, - "acceptEmergencyAccessInviteToken": undefined, - "email": "test@email.com", - "emailVerificationToken": "emailVerificationToken", - "masterPasswordAuthentication": { - "kdf": PBKDF2KdfConfig { - "iterations": 600000, - "kdfType": 0, - }, - "masterPasswordAuthenticationHash": "authHash", - "salt": "salt", - }, - "masterPasswordHint": "newPasswordHint", - "masterPasswordUnlock": { - "kdf": PBKDF2KdfConfig { - "iterations": 600000, - "kdfType": 0, - }, - "masterKeyWrappedUserKey": "masterKeyWrappedUserKey", - "salt": "salt", - }, - "orgInviteToken": undefined, - "orgSponsoredFreeFamilyPlanToken": undefined, - "organizationUserId": undefined, - "providerInviteToken": undefined, - "providerUserId": undefined, - "userAsymmetricKeys": KeysRequest { - "encryptedPrivateKey": "privateKey", - "publicKey": "publicKey", - }, -} -`; - -exports[`WebRegistrationFinishService finishRegistration() when feature flag is ON (new API) it registers the user with org invite when given an org invite 1`] = ` -RegisterFinishRequestWithAuthUnlockDataTypes { - "acceptEmergencyAccessId": undefined, - "acceptEmergencyAccessInviteToken": undefined, - "email": "test@email.com", - "emailVerificationToken": undefined, - "masterPasswordAuthentication": { - "kdf": PBKDF2KdfConfig { - "iterations": 600000, - "kdfType": 0, - }, - "masterPasswordAuthenticationHash": "authHash", - "salt": "salt", - }, - "masterPasswordHint": "newPasswordHint", - "masterPasswordUnlock": { - "kdf": PBKDF2KdfConfig { - "iterations": 600000, - "kdfType": 0, - }, - "masterKeyWrappedUserKey": "masterKeyWrappedUserKey", - "salt": "salt", - }, - "orgInviteToken": "orgInviteToken", - "orgSponsoredFreeFamilyPlanToken": undefined, - "organizationUserId": "organizationUserId", - "providerInviteToken": undefined, - "providerUserId": undefined, - "userAsymmetricKeys": KeysRequest { - "encryptedPrivateKey": "privateKey", - "publicKey": "publicKey", - }, -} -`; - -exports[`WebRegistrationFinishService finishRegistration() when feature flag is ON (new API) registers the user when given a provider invite token 1`] = ` -RegisterFinishRequestWithAuthUnlockDataTypes { - "acceptEmergencyAccessId": undefined, - "acceptEmergencyAccessInviteToken": undefined, - "email": "test@email.com", - "emailVerificationToken": undefined, - "masterPasswordAuthentication": { - "kdf": PBKDF2KdfConfig { - "iterations": 600000, - "kdfType": 0, - }, - "masterPasswordAuthenticationHash": "authHash", - "salt": "salt", - }, - "masterPasswordHint": "newPasswordHint", - "masterPasswordUnlock": { - "kdf": PBKDF2KdfConfig { - "iterations": 600000, - "kdfType": 0, - }, - "masterKeyWrappedUserKey": "masterKeyWrappedUserKey", - "salt": "salt", - }, - "orgInviteToken": undefined, - "orgSponsoredFreeFamilyPlanToken": undefined, - "organizationUserId": undefined, - "providerInviteToken": "providerInviteToken", - "providerUserId": "providerUserId", - "userAsymmetricKeys": KeysRequest { - "encryptedPrivateKey": "privateKey", - "publicKey": "publicKey", - }, -} -`; - -exports[`WebRegistrationFinishService finishRegistration() when feature flag is ON (new API) registers the user when given an emergency access invite token 1`] = ` -RegisterFinishRequestWithAuthUnlockDataTypes { - "acceptEmergencyAccessId": "emergencyAccessId", - "acceptEmergencyAccessInviteToken": "acceptEmergencyAccessInviteToken", - "email": "test@email.com", - "emailVerificationToken": undefined, - "masterPasswordAuthentication": { - "kdf": PBKDF2KdfConfig { - "iterations": 600000, - "kdfType": 0, - }, - "masterPasswordAuthenticationHash": "authHash", - "salt": "salt", - }, - "masterPasswordHint": "newPasswordHint", - "masterPasswordUnlock": { - "kdf": PBKDF2KdfConfig { - "iterations": 600000, - "kdfType": 0, - }, - "masterKeyWrappedUserKey": "masterKeyWrappedUserKey", - "salt": "salt", - }, - "orgInviteToken": undefined, - "orgSponsoredFreeFamilyPlanToken": undefined, - "organizationUserId": undefined, - "providerInviteToken": undefined, - "providerUserId": undefined, - "userAsymmetricKeys": KeysRequest { - "encryptedPrivateKey": "privateKey", - "publicKey": "publicKey", - }, -} -`; - -exports[`WebRegistrationFinishService finishRegistration() when feature flag is ON (new API) registers the user when given an org sponsored free family plan token 1`] = ` -RegisterFinishRequestWithAuthUnlockDataTypes { - "acceptEmergencyAccessId": undefined, - "acceptEmergencyAccessInviteToken": undefined, - "email": "test@email.com", - "emailVerificationToken": undefined, - "masterPasswordAuthentication": { - "kdf": PBKDF2KdfConfig { - "iterations": 600000, - "kdfType": 0, - }, - "masterPasswordAuthenticationHash": "authHash", - "salt": "salt", - }, - "masterPasswordHint": "newPasswordHint", - "masterPasswordUnlock": { - "kdf": PBKDF2KdfConfig { - "iterations": 600000, - "kdfType": 0, - }, - "masterKeyWrappedUserKey": "masterKeyWrappedUserKey", - "salt": "salt", - }, - "orgInviteToken": undefined, - "orgSponsoredFreeFamilyPlanToken": "orgSponsoredFreeFamilyPlanToken", - "organizationUserId": undefined, - "providerInviteToken": undefined, - "providerUserId": undefined, - "userAsymmetricKeys": KeysRequest { - "encryptedPrivateKey": "privateKey", - "publicKey": "publicKey", - }, -} -`; diff --git a/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.spec.ts b/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.spec.ts index 08061a9e4fff..10d43401dfde 100644 --- a/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.spec.ts +++ b/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.spec.ts @@ -10,7 +10,6 @@ import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/mod import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-api.service"; import { RegisterFinishRequestWithAuthUnlockDataTypes } from "@bitwarden/common/auth/models/request/registration/register-finish-request-with-auth-unlock-data.types"; -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 { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; @@ -22,7 +21,6 @@ import { MasterPasswordAuthenticationHash, MasterKeyWrappedUserKey, } 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 { MasterKey, UserKey } from "@bitwarden/common/types/key"; @@ -40,7 +38,6 @@ describe("WebRegistrationFinishService", () => { let logService: MockProxy; let policyService: MockProxy; let masterPasswordService: MockProxy; - let configService: MockProxy; beforeEach(() => { keyService = mock(); @@ -50,13 +47,11 @@ describe("WebRegistrationFinishService", () => { logService = mock(); policyService = mock(); masterPasswordService = mock(); - configService = mock(); service = new WebRegistrationFinishService( keyService, accountApiService, masterPasswordService, - configService, organizationInviteService, policyApiService, logService, @@ -196,17 +191,17 @@ describe("WebRegistrationFinishService", () => { email = "test@email.com"; emailVerificationToken = "emailVerificationToken"; masterKey = new SymmetricCryptoKey(new Uint8Array(64)) as MasterKey; + salt = "salt" as MasterPasswordSalt; + passwordInputResult = { - newMasterKey: masterKey, - newServerMasterKeyHash: "newServerMasterKeyHash", + newPassword: "newPassword", kdfConfig: DEFAULT_KDF_CONFIG, newPasswordHint: "newPasswordHint", - newPassword: "newPassword", + salt: salt, }; userKey = new SymmetricCryptoKey(new Uint8Array(64)) as UserKey; userKeyEncString = new EncString("userKeyEncrypted"); - userKeyPair = ["publicKey", new EncString("privateKey")]; orgInvite = new OrganizationInvite(); @@ -219,12 +214,12 @@ describe("WebRegistrationFinishService", () => { providerInviteToken = "providerInviteToken"; providerUserId = "providerUserId"; + keyService.makeMasterKey.mockResolvedValue(masterKey); keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]); keyService.makeKeyPair.mockResolvedValue(userKeyPair); accountApiService.registerFinish.mockResolvedValue(); organizationInviteService.getOrganizationInvite.mockResolvedValue(null); - salt = "salt" as MasterPasswordSalt; masterPasswordAuthentication = { salt, kdf: DEFAULT_KDF_CONFIG, @@ -235,6 +230,10 @@ describe("WebRegistrationFinishService", () => { DEFAULT_KDF_CONFIG, "masterKeyWrappedUserKey" as MasterKeyWrappedUserKey, ); + masterPasswordService.makeMasterPasswordAuthenticationData.mockResolvedValue( + masterPasswordAuthentication, + ); + masterPasswordService.makeMasterPasswordUnlockData.mockResolvedValue(masterPasswordUnlock); }); it("throws an error if the user key cannot be created", async () => { @@ -245,272 +244,136 @@ describe("WebRegistrationFinishService", () => { ); }); - describe("when feature flag is OFF (old API)", () => { - it("registers the user with KDF fields when given valid email verification input", async () => { - await service.finishRegistration(email, passwordInputResult, emailVerificationToken); - - expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey); - expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey); - - const registerCall = accountApiService.registerFinish.mock - .calls[0][0] as RegisterFinishRequest; - expect(registerCall).toBeInstanceOf(RegisterFinishRequest); - - // Old API sends flat KDF and master password hash fields - expect(registerCall.kdf).toBeDefined(); - expect(registerCall.kdfIterations).toBeDefined(); - expect(registerCall.masterPasswordHash).toBeDefined(); - expect(registerCall.userSymmetricKey).toBeDefined(); - - // Unique to this flow: emailVerificationToken is populated - expect(registerCall.emailVerificationToken).toEqual(emailVerificationToken); - - expect(registerCall).toMatchSnapshot(); - }); - - it("it registers the user with org invite when given an org invite", async () => { - organizationInviteService.getOrganizationInvite.mockResolvedValue(orgInvite); - - await service.finishRegistration(email, passwordInputResult); - - const registerCall = accountApiService.registerFinish.mock - .calls[0][0] as RegisterFinishRequest; - expect(registerCall).toBeInstanceOf(RegisterFinishRequest); - - // Unique to this flow: org invite fields are populated - expect(registerCall.orgInviteToken).toEqual(orgInvite.token); - expect(registerCall.organizationUserId).toEqual(orgInvite.organizationUserId); - - expect(registerCall).toMatchSnapshot(); - }); - - it("registers the user when given an org sponsored free family plan token", async () => { - await service.finishRegistration( - email, - passwordInputResult, - undefined, - orgSponsoredFreeFamilyPlanToken, - ); - - const registerCall = accountApiService.registerFinish.mock - .calls[0][0] as RegisterFinishRequest; - expect(registerCall).toBeInstanceOf(RegisterFinishRequest); - - // Unique to this flow: org sponsored free family plan token is populated - expect(registerCall.orgSponsoredFreeFamilyPlanToken).toEqual( - orgSponsoredFreeFamilyPlanToken, - ); - - expect(registerCall).toMatchSnapshot(); - }); - - it("registers the user when given an emergency access invite token", async () => { - await service.finishRegistration( - email, - passwordInputResult, - undefined, - undefined, - acceptEmergencyAccessInviteToken, - emergencyAccessId, - ); - - expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey); - expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey); - - const registerCall = accountApiService.registerFinish.mock - .calls[0][0] as RegisterFinishRequest; - expect(registerCall).toBeInstanceOf(RegisterFinishRequest); - - // Unique to this flow: emergency access fields are populated - expect(registerCall.acceptEmergencyAccessInviteToken).toEqual( - acceptEmergencyAccessInviteToken, - ); - expect(registerCall.acceptEmergencyAccessId).toEqual(emergencyAccessId); - - expect(registerCall).toMatchSnapshot(); - }); - - it("registers the user when given a provider invite token", async () => { - await service.finishRegistration( - email, - passwordInputResult, - undefined, - undefined, - undefined, - undefined, - providerInviteToken, - providerUserId, - ); - - expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey); - expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey); - - const registerCall = accountApiService.registerFinish.mock - .calls[0][0] as RegisterFinishRequest; - expect(registerCall).toBeInstanceOf(RegisterFinishRequest); - - // Unique to this flow: provider invite fields are populated - expect(registerCall.providerInviteToken).toEqual(providerInviteToken); - expect(registerCall.providerUserId).toEqual(providerUserId); - - expect(registerCall).toMatchSnapshot(); - }); + it("derives the master key and registers the user with new data types", async () => { + await service.finishRegistration(email, passwordInputResult, emailVerificationToken); + + // Verify master key is derived internally + expect(keyService.makeMasterKey).toHaveBeenCalledWith( + passwordInputResult.newPassword, + passwordInputResult.salt, + passwordInputResult.kdfConfig, + ); + expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey); + expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey); + + const registerCall = accountApiService.registerFinish.mock + .calls[0][0] as RegisterFinishRequestWithAuthUnlockDataTypes; + expect(registerCall).toBeInstanceOf(RegisterFinishRequestWithAuthUnlockDataTypes); + + // New API sends structured authentication and unlock data + expect(registerCall.masterPasswordAuthentication).toBeDefined(); + expect(registerCall.masterPasswordUnlock).toBeDefined(); + + // Old API flat fields must NOT be present + expect((registerCall as any).masterPasswordHash).toBeUndefined(); + expect((registerCall as any).userSymmetricKey).toBeUndefined(); + expect((registerCall as any).kdf).toBeUndefined(); + expect((registerCall as any).kdfIterations).toBeUndefined(); + + // Unique to this flow: emailVerificationToken is populated + expect(registerCall.emailVerificationToken).toEqual(emailVerificationToken); + + expect(registerCall).toMatchSnapshot(); + }); + + it("it registers the user with org invite when given an org invite", async () => { + organizationInviteService.getOrganizationInvite.mockResolvedValue(orgInvite); + + await service.finishRegistration(email, passwordInputResult); + + expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey); + expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey); + + const registerCall = accountApiService.registerFinish.mock + .calls[0][0] as RegisterFinishRequestWithAuthUnlockDataTypes; + expect(registerCall).toBeInstanceOf(RegisterFinishRequestWithAuthUnlockDataTypes); + expect(registerCall.masterPasswordAuthentication).toBeDefined(); + expect(registerCall.masterPasswordUnlock).toBeDefined(); + + // Unique to this flow: org invite fields are populated + expect(registerCall.orgInviteToken).toEqual(orgInvite.token); + expect(registerCall.organizationUserId).toEqual(orgInvite.organizationUserId); + + expect(registerCall).toMatchSnapshot(); + }); + + it("registers the user when given an org sponsored free family plan token", async () => { + await service.finishRegistration( + email, + passwordInputResult, + undefined, + orgSponsoredFreeFamilyPlanToken, + ); + + expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey); + expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey); + + const registerCall = accountApiService.registerFinish.mock + .calls[0][0] as RegisterFinishRequestWithAuthUnlockDataTypes; + expect(registerCall).toBeInstanceOf(RegisterFinishRequestWithAuthUnlockDataTypes); + expect(registerCall.masterPasswordAuthentication).toBeDefined(); + expect(registerCall.masterPasswordUnlock).toBeDefined(); + + // Unique to this flow: org sponsored free family plan token is populated + expect(registerCall.orgSponsoredFreeFamilyPlanToken).toEqual(orgSponsoredFreeFamilyPlanToken); + + expect(registerCall).toMatchSnapshot(); + }); + + it("registers the user when given an emergency access invite token", async () => { + await service.finishRegistration( + email, + passwordInputResult, + undefined, + undefined, + acceptEmergencyAccessInviteToken, + emergencyAccessId, + ); + + expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey); + expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey); + + const registerCall = accountApiService.registerFinish.mock + .calls[0][0] as RegisterFinishRequestWithAuthUnlockDataTypes; + expect(registerCall).toBeInstanceOf(RegisterFinishRequestWithAuthUnlockDataTypes); + expect(registerCall.masterPasswordAuthentication).toBeDefined(); + expect(registerCall.masterPasswordUnlock).toBeDefined(); + + // Unique to this flow: emergency access fields are populated + expect(registerCall.acceptEmergencyAccessInviteToken).toEqual( + acceptEmergencyAccessInviteToken, + ); + expect(registerCall.acceptEmergencyAccessId).toEqual(emergencyAccessId); + + expect(registerCall).toMatchSnapshot(); }); - describe("when feature flag is ON (new API)", () => { - beforeEach(() => { - // When the Auth flag is ON, InputPasswordComponent emits newApisWithInputPasswordFlagEnabled: true - // and does NOT emit newMasterKey, newServerMasterKeyHash. - passwordInputResult = { - newPassword: "newPassword", - kdfConfig: DEFAULT_KDF_CONFIG, - newPasswordHint: "newPasswordHint", - newApisWithInputPasswordFlagEnabled: true, - salt: salt, - }; - - // The service derives the master key internally when the Auth flag is ON - keyService.makeMasterKey.mockResolvedValue(masterKey); - - masterPasswordService.makeMasterPasswordAuthenticationData.mockResolvedValue( - masterPasswordAuthentication, - ); - masterPasswordService.makeMasterPasswordUnlockData.mockResolvedValue(masterPasswordUnlock); - }); - - it("derives the master key and registers the user with new data types", async () => { - await service.finishRegistration(email, passwordInputResult, emailVerificationToken); - - // Verify master key is derived internally - expect(keyService.makeMasterKey).toHaveBeenCalledWith( - passwordInputResult.newPassword, - passwordInputResult.salt, - passwordInputResult.kdfConfig, - ); - expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey); - expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey); - - const registerCall = accountApiService.registerFinish.mock - .calls[0][0] as RegisterFinishRequestWithAuthUnlockDataTypes; - expect(registerCall).toBeInstanceOf(RegisterFinishRequestWithAuthUnlockDataTypes); - - // New API sends structured authentication and unlock data - expect(registerCall.masterPasswordAuthentication).toBeDefined(); - expect(registerCall.masterPasswordUnlock).toBeDefined(); - - // Old API flat fields must NOT be present - expect((registerCall as any).masterPasswordHash).toBeUndefined(); - expect((registerCall as any).userSymmetricKey).toBeUndefined(); - expect((registerCall as any).kdf).toBeUndefined(); - expect((registerCall as any).kdfIterations).toBeUndefined(); - - // Unique to this flow: emailVerificationToken is populated - expect(registerCall.emailVerificationToken).toEqual(emailVerificationToken); - - expect(registerCall).toMatchSnapshot(); - }); - - it("it registers the user with org invite when given an org invite", async () => { - organizationInviteService.getOrganizationInvite.mockResolvedValue(orgInvite); - - await service.finishRegistration(email, passwordInputResult); - - expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey); - expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey); - - const registerCall = accountApiService.registerFinish.mock - .calls[0][0] as RegisterFinishRequestWithAuthUnlockDataTypes; - expect(registerCall).toBeInstanceOf(RegisterFinishRequestWithAuthUnlockDataTypes); - expect(registerCall.masterPasswordAuthentication).toBeDefined(); - expect(registerCall.masterPasswordUnlock).toBeDefined(); - - // Unique to this flow: org invite fields are populated - expect(registerCall.orgInviteToken).toEqual(orgInvite.token); - expect(registerCall.organizationUserId).toEqual(orgInvite.organizationUserId); - - expect(registerCall).toMatchSnapshot(); - }); - - it("registers the user when given an org sponsored free family plan token", async () => { - await service.finishRegistration( - email, - passwordInputResult, - undefined, - orgSponsoredFreeFamilyPlanToken, - ); - - expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey); - expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey); - - const registerCall = accountApiService.registerFinish.mock - .calls[0][0] as RegisterFinishRequestWithAuthUnlockDataTypes; - expect(registerCall).toBeInstanceOf(RegisterFinishRequestWithAuthUnlockDataTypes); - expect(registerCall.masterPasswordAuthentication).toBeDefined(); - expect(registerCall.masterPasswordUnlock).toBeDefined(); - - // Unique to this flow: org sponsored free family plan token is populated - expect(registerCall.orgSponsoredFreeFamilyPlanToken).toEqual( - orgSponsoredFreeFamilyPlanToken, - ); - - expect(registerCall).toMatchSnapshot(); - }); - - it("registers the user when given an emergency access invite token", async () => { - await service.finishRegistration( - email, - passwordInputResult, - undefined, - undefined, - acceptEmergencyAccessInviteToken, - emergencyAccessId, - ); - - expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey); - expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey); - - const registerCall = accountApiService.registerFinish.mock - .calls[0][0] as RegisterFinishRequestWithAuthUnlockDataTypes; - expect(registerCall).toBeInstanceOf(RegisterFinishRequestWithAuthUnlockDataTypes); - expect(registerCall.masterPasswordAuthentication).toBeDefined(); - expect(registerCall.masterPasswordUnlock).toBeDefined(); - - // Unique to this flow: emergency access fields are populated - expect(registerCall.acceptEmergencyAccessInviteToken).toEqual( - acceptEmergencyAccessInviteToken, - ); - expect(registerCall.acceptEmergencyAccessId).toEqual(emergencyAccessId); - - expect(registerCall).toMatchSnapshot(); - }); - - it("registers the user when given a provider invite token", async () => { - await service.finishRegistration( - email, - passwordInputResult, - undefined, - undefined, - undefined, - undefined, - providerInviteToken, - providerUserId, - ); - - expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey); - expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey); - - const registerCall = accountApiService.registerFinish.mock - .calls[0][0] as RegisterFinishRequestWithAuthUnlockDataTypes; - expect(registerCall).toBeInstanceOf(RegisterFinishRequestWithAuthUnlockDataTypes); - expect(registerCall.masterPasswordAuthentication).toBeDefined(); - expect(registerCall.masterPasswordUnlock).toBeDefined(); - - // Unique to this flow: provider invite fields are populated - expect(registerCall.providerInviteToken).toEqual(providerInviteToken); - expect(registerCall.providerUserId).toEqual(providerUserId); - - expect(registerCall).toMatchSnapshot(); - }); + it("registers the user when given a provider invite token", async () => { + await service.finishRegistration( + email, + passwordInputResult, + undefined, + undefined, + undefined, + undefined, + providerInviteToken, + providerUserId, + ); + + expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey); + expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey); + + const registerCall = accountApiService.registerFinish.mock + .calls[0][0] as RegisterFinishRequestWithAuthUnlockDataTypes; + expect(registerCall).toBeInstanceOf(RegisterFinishRequestWithAuthUnlockDataTypes); + expect(registerCall.masterPasswordAuthentication).toBeDefined(); + expect(registerCall.masterPasswordUnlock).toBeDefined(); + + // Unique to this flow: provider invite fields are populated + expect(registerCall.providerInviteToken).toEqual(providerInviteToken); + expect(registerCall.providerUserId).toEqual(providerUserId); + + expect(registerCall).toMatchSnapshot(); }); }); }); diff --git a/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.ts b/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.ts index 10fdde602064..03c14b5412ef 100644 --- a/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.ts +++ b/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.ts @@ -13,14 +13,9 @@ import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/mod import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-api.service"; import { RegisterFinishRequestWithAuthUnlockDataTypes } from "@bitwarden/common/auth/models/request/registration/register-finish-request-with-auth-unlock-data.types"; -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 { - EncryptedString, - EncString, -} from "@bitwarden/common/key-management/crypto/models/enc-string"; +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"; @@ -33,13 +28,12 @@ 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, configService); + super(keyService, accountApiService, masterPasswordService); } override async getOrgNameFromOrgInvite(): Promise { @@ -87,7 +81,6 @@ export class WebRegistrationFinishService newUserKey: UserKey, email: string, passwordInputResult: PasswordInputResult, - encryptedUserKey: EncryptedString, userAsymmetricKeys: [string, EncString], emailVerificationToken?: string, orgSponsoredFreeFamilyPlanToken?: string, @@ -95,12 +88,11 @@ export class WebRegistrationFinishService emergencyAccessId?: string, providerInviteToken?: string, providerUserId?: string, - ): Promise { + ): Promise { const registerRequest = await super.buildRegisterRequest( newUserKey, email, passwordInputResult, - encryptedUserKey, userAsymmetricKeys, emailVerificationToken, ); diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index c8a65f0377af..424f35fdc2f5 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -294,7 +294,6 @@ const safeProviders: SafeProvider[] = [ KeyServiceAbstraction, AccountApiServiceAbstraction, MasterPasswordServiceAbstraction, - ConfigService, OrganizationInviteService, PolicyApiServiceAbstraction, LogService, diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index ed1051ac18d7..8354239a0418 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -1683,12 +1683,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: RegistrationFinishServiceAbstraction, useClass: DefaultRegistrationFinishService, - deps: [ - KeyService, - AccountApiServiceAbstraction, - MasterPasswordServiceAbstraction, - ConfigService, - ], + deps: [KeyService, AccountApiServiceAbstraction, MasterPasswordServiceAbstraction], }), safeProvider({ provide: TwoFactorAuthComponentService, diff --git a/libs/auth/src/angular/registration/registration-finish/__snapshots__/default-registration-finish.service.spec.ts.snap b/libs/auth/src/angular/registration/registration-finish/__snapshots__/default-registration-finish.service.spec.ts.snap deleted file mode 100644 index 6a82899dcc18..000000000000 --- a/libs/auth/src/angular/registration/registration-finish/__snapshots__/default-registration-finish.service.spec.ts.snap +++ /dev/null @@ -1,61 +0,0 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing - -exports[`DefaultRegistrationFinishService finishRegistration() when feature flag is OFF (old API) registers the user with KDF fields when given valid email verification input 1`] = ` -RegisterFinishRequest { - "acceptEmergencyAccessId": undefined, - "acceptEmergencyAccessInviteToken": undefined, - "email": "test@email.com", - "emailVerificationToken": "emailVerificationToken", - "kdf": 0, - "kdfIterations": 600000, - "kdfMemory": undefined, - "kdfParallelism": undefined, - "masterPasswordHash": "newServerMasterKeyHash", - "masterPasswordHint": "newPasswordHint", - "orgInviteToken": undefined, - "orgSponsoredFreeFamilyPlanToken": undefined, - "organizationUserId": undefined, - "providerInviteToken": undefined, - "providerUserId": undefined, - "userAsymmetricKeys": KeysRequest { - "encryptedPrivateKey": "privateKey", - "publicKey": "publicKey", - }, - "userSymmetricKey": "userKeyEncrypted", -} -`; - -exports[`DefaultRegistrationFinishService finishRegistration() when feature flag is ON (new API) derives the master key and registers the user with new data types 1`] = ` -RegisterFinishRequestWithAuthUnlockDataTypes { - "acceptEmergencyAccessId": undefined, - "acceptEmergencyAccessInviteToken": undefined, - "email": "test@email.com", - "emailVerificationToken": "emailVerificationToken", - "masterPasswordAuthentication": { - "kdf": PBKDF2KdfConfig { - "iterations": 600000, - "kdfType": 0, - }, - "masterPasswordAuthenticationHash": "authHash", - "salt": "test@email.com", - }, - "masterPasswordHint": "newPasswordHint", - "masterPasswordUnlock": { - "kdf": PBKDF2KdfConfig { - "iterations": 600000, - "kdfType": 0, - }, - "masterKeyWrappedUserKey": "wrappedUserKey", - "salt": "test@email.com", - }, - "orgInviteToken": undefined, - "orgSponsoredFreeFamilyPlanToken": undefined, - "organizationUserId": undefined, - "providerInviteToken": undefined, - "providerUserId": undefined, - "userAsymmetricKeys": KeysRequest { - "encryptedPrivateKey": "privateKey", - "publicKey": "publicKey", - }, -} -`; diff --git a/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.spec.ts b/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.spec.ts index 3e190f71afa5..211bf19e11af 100644 --- a/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.spec.ts +++ b/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.spec.ts @@ -2,7 +2,6 @@ import { MockProxy, mock } from "jest-mock-extended"; import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-api.service"; import { RegisterFinishRequestWithAuthUnlockDataTypes } from "@bitwarden/common/auth/models/request/registration/register-finish-request-with-auth-unlock-data.types"; -import { RegisterFinishRequest } from "@bitwarden/common/auth/models/request/registration/register-finish.request"; 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 { @@ -12,10 +11,9 @@ 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 { MasterKey, UserKey } from "@bitwarden/common/types/key"; -import { DEFAULT_KDF_CONFIG, KeyService, KdfType } from "@bitwarden/key-management"; +import { DEFAULT_KDF_CONFIG, KeyService } from "@bitwarden/key-management"; import { PasswordInputResult } from "../../input-password/password-input-result"; @@ -27,19 +25,16 @@ describe("DefaultRegistrationFinishService", () => { let keyService: MockProxy; let accountApiService: MockProxy; let masterPasswordService: MockProxy; - let configService: MockProxy; beforeEach(() => { keyService = mock(); accountApiService = mock(); masterPasswordService = mock(); - configService = mock(); service = new DefaultRegistrationFinishService( keyService, accountApiService, masterPasswordService, - configService, ); }); @@ -71,163 +66,96 @@ describe("DefaultRegistrationFinishService", () => { let userKey: UserKey; let userKeyEncString: EncString; let userKeyPair: [string, EncString]; + let salt: MasterPasswordSalt; + let masterPasswordAuthentication: MasterPasswordAuthenticationData; + let masterPasswordUnlock: MasterPasswordUnlockData; beforeEach(() => { email = "test@email.com"; emailVerificationToken = "emailVerificationToken"; masterKey = new SymmetricCryptoKey(new Uint8Array(64)) as MasterKey; + salt = "test@email.com" as MasterPasswordSalt; + passwordInputResult = { - newMasterKey: masterKey, - newServerMasterKeyHash: "newServerMasterKeyHash", + newPassword: "newPassword", kdfConfig: DEFAULT_KDF_CONFIG, newPasswordHint: "newPasswordHint", - newPassword: "newPassword", + salt: salt, }; userKey = new SymmetricCryptoKey(new Uint8Array(64)) as UserKey; userKeyEncString = new EncString("userKeyEncrypted"); - userKeyPair = ["publicKey", new EncString("privateKey")]; + + keyService.makeMasterKey.mockResolvedValue(masterKey); + + masterPasswordAuthentication = { + salt, + kdf: DEFAULT_KDF_CONFIG, + masterPasswordAuthenticationHash: "authHash" as MasterPasswordAuthenticationHash, + }; + masterPasswordUnlock = new MasterPasswordUnlockData( + salt, + DEFAULT_KDF_CONFIG, + "wrappedUserKey" as MasterKeyWrappedUserKey, + ); + masterPasswordService.makeMasterPasswordAuthenticationData.mockResolvedValue( + masterPasswordAuthentication, + ); + masterPasswordService.makeMasterPasswordUnlockData.mockResolvedValue(masterPasswordUnlock); }); it("throws an error if the user key cannot be created", async () => { - keyService.makeUserKey.mockResolvedValue([null, null]); + keyService.makeUserKey.mockResolvedValue([null, null] as any); await expect(service.finishRegistration(email, passwordInputResult)).rejects.toThrow( "User key could not be created", ); }); - describe("when feature flag is OFF (old API)", () => { - 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(); - - await service.finishRegistration(email, passwordInputResult, emailVerificationToken); - - 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, - }, - kdf: KdfType.PBKDF2_SHA256, - kdfIterations: DEFAULT_KDF_CONFIG.iterations, - kdfMemory: undefined, - kdfParallelism: undefined, - }), - ); - - // Verify makeMasterKey is NOT called — the master key comes from passwordInputResult directly - expect(keyService.makeMasterKey).not.toHaveBeenCalled(); - - // 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(); - - expect(registerCall).toMatchSnapshot(); - }); - }); + it("derives the master key and registers the user with new data types", async () => { + keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]); + keyService.makeKeyPair.mockResolvedValue(userKeyPair); + accountApiService.registerFinish.mockResolvedValue(); + + await service.finishRegistration(email, passwordInputResult, emailVerificationToken); + + expect(keyService.makeMasterKey).toHaveBeenCalledWith( + passwordInputResult.newPassword, + passwordInputResult.salt, + passwordInputResult.kdfConfig, + ); + expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey); + expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey); + expect(accountApiService.registerFinish).toHaveBeenCalledWith( + expect.objectContaining({ + email, + emailVerificationToken: emailVerificationToken, + masterPasswordHint: passwordInputResult.newPasswordHint, + userAsymmetricKeys: { + publicKey: userKeyPair[0], + encryptedPrivateKey: userKeyPair[1].encryptedString, + }, + masterPasswordAuthentication: masterPasswordAuthentication, + masterPasswordUnlock: masterPasswordUnlock, + }), + ); - describe("when feature flag is ON (new API)", () => { - let salt: MasterPasswordSalt; - let masterPasswordAuthentication: MasterPasswordAuthenticationData; - let masterPasswordUnlock: MasterPasswordUnlockData; - - beforeEach(() => { - salt = "test@email.com" as MasterPasswordSalt; - - // When the Auth flag is ON, InputPasswordComponent emits newApisWithInputPasswordFlagEnabled: true - // and does NOT emit newMasterKey, newServerMasterKeyHash. - passwordInputResult = { - newPassword: "newPassword", - kdfConfig: DEFAULT_KDF_CONFIG, - newPasswordHint: "newPasswordHint", - newApisWithInputPasswordFlagEnabled: true, - salt: salt, - }; - - // The service derives the master key internally when the Auth flag is ON - keyService.makeMasterKey.mockResolvedValue(masterKey); - - masterPasswordAuthentication = { - salt, - kdf: DEFAULT_KDF_CONFIG, - masterPasswordAuthenticationHash: "authHash" as MasterPasswordAuthenticationHash, - }; - masterPasswordUnlock = new MasterPasswordUnlockData( - salt, - DEFAULT_KDF_CONFIG, - "wrappedUserKey" as MasterKeyWrappedUserKey, - ); - masterPasswordService.makeMasterPasswordAuthenticationData.mockResolvedValue( - masterPasswordAuthentication, - ); - masterPasswordService.makeMasterPasswordUnlockData.mockResolvedValue(masterPasswordUnlock); - }); - - it("derives the master key and registers the user with new data types", async () => { - keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]); - keyService.makeKeyPair.mockResolvedValue(userKeyPair); - accountApiService.registerFinish.mockResolvedValue(); - - await service.finishRegistration(email, passwordInputResult, emailVerificationToken); - - // Verify master key is derived internally (not from passwordInputResult) - expect(keyService.makeMasterKey).toHaveBeenCalledWith( - passwordInputResult.newPassword, - passwordInputResult.salt, - passwordInputResult.kdfConfig, - ); - expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey); - expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey); - expect(accountApiService.registerFinish).toHaveBeenCalledWith( - expect.objectContaining({ - email, - emailVerificationToken: emailVerificationToken, - masterPasswordHint: passwordInputResult.newPasswordHint, - 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(RegisterFinishRequestWithAuthUnlockDataTypes); - expect( - (registerCall as RegisterFinishRequestWithAuthUnlockDataTypes) - .masterPasswordAuthentication, - ).toBeDefined(); - expect( - (registerCall as RegisterFinishRequestWithAuthUnlockDataTypes).masterPasswordUnlock, - ).toBeDefined(); - - // Verify old API fields are NOT present (including masterPasswordHash which is in masterPasswordAuthentication) - expect((registerCall as any).masterPasswordHash).toBeUndefined(); - expect((registerCall as any).userSymmetricKey).toBeUndefined(); - expect((registerCall as any).kdf).toBeUndefined(); - expect((registerCall as any).kdfIterations).toBeUndefined(); - - expect(registerCall).toMatchSnapshot(); - }); + const registerCall = accountApiService.registerFinish.mock.calls[0][0]; + expect(registerCall).toBeInstanceOf(RegisterFinishRequestWithAuthUnlockDataTypes); + expect( + (registerCall as RegisterFinishRequestWithAuthUnlockDataTypes).masterPasswordAuthentication, + ).toBeDefined(); + expect( + (registerCall as RegisterFinishRequestWithAuthUnlockDataTypes).masterPasswordUnlock, + ).toBeDefined(); + + expect((registerCall as any).masterPasswordHash).toBeUndefined(); + expect((registerCall as any).userSymmetricKey).toBeUndefined(); + expect((registerCall as any).kdf).toBeUndefined(); + expect((registerCall as any).kdfIterations).toBeUndefined(); + + expect(registerCall).toMatchSnapshot(); }); }); }); diff --git a/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.ts b/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.ts index 43c73e0b68df..28325d9551ce 100644 --- a/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.ts +++ b/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.ts @@ -3,15 +3,10 @@ import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-api.service"; import { RegisterFinishRequestWithAuthUnlockDataTypes } from "@bitwarden/common/auth/models/request/registration/register-finish-request-with-auth-unlock-data.types"; -import { RegisterFinishRequest } from "@bitwarden/common/auth/models/request/registration/register-finish.request"; import { assertNonNullish, assertTruthy } from "@bitwarden/common/auth/utils"; -import { - EncryptedString, - EncString, -} from "@bitwarden/common/key-management/crypto/models/enc-string"; +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 { 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"; @@ -24,7 +19,6 @@ export class DefaultRegistrationFinishService implements RegistrationFinishServi protected keyService: KeyService, protected accountApiService: AccountApiService, protected masterPasswordService: MasterPasswordServiceAbstraction, - protected configService: ConfigService, ) {} getOrgNameFromOrgInvite(): Promise { @@ -45,56 +39,18 @@ export class DefaultRegistrationFinishService implements RegistrationFinishServi providerInviteToken?: string, providerUserId?: string, ): Promise { - /** - * "KM flag" = (KM team has ongoing work for this here: PM-24223) - * "Auth flag" = PM27086_UpdateAuthenticationApisForInputPassword (checked in InputPasswordComponent - * and passed through via PasswordInputResult.newApisWithInputPasswordFlagEnabled) - * - * Flag unwinding will depend on which flag gets unwound first: - * - If KM flag gets unwound first, remove all code after the KM V2 path, - * as the V2Encryption method is the end-goal. - * - If Auth flag gets unwound first (in PM-28143), keep the KM code & early return, - * but unwind the auth flagging logic and remove the unflagged `else` block logic following - * the "Scenario 2" code. - */ - - // Scenario 1: KM V2 flag ON (placeholder — to be added when KM's registration V2 PR lands) - // const accountEncryptionV2 = await this.configService.getFeatureFlag( - // FeatureFlag.EnableAccountEncryptionV2Registration, // flag name TBD by KM team - // ); - // if (accountEncryptionV2) { - // // SDK path — end goal. Replaces all key derivation below. - // return; - // } - - let newUserKey: UserKey; - let newEncUserKey: EncString; - - // Scenario 2: KM flag OFF, Auth flag ON - if (passwordInputResult.newApisWithInputPasswordFlagEnabled) { - /** - * If the Auth flag is enabled, it means the InputPasswordComponent will not emit a newMasterKey. - * So we must create it here for registration. - * - * This is a temporary state. The end-goal will be to use KM's V2Encryption method above. - */ - const ctx = "Could not finish registration."; - assertTruthy(passwordInputResult.newPassword, "newPassword", ctx); - assertNonNullish(passwordInputResult.kdfConfig, "kdfConfig", ctx); - assertTruthy(passwordInputResult.salt, "salt", ctx); - - const newMasterKey = await this.keyService.makeMasterKey( - passwordInputResult.newPassword, - passwordInputResult.salt, - passwordInputResult.kdfConfig, - ); + const ctx = "Could not finish registration."; + assertTruthy(passwordInputResult.newPassword, "newPassword", ctx); + assertNonNullish(passwordInputResult.kdfConfig, "kdfConfig", ctx); + assertTruthy(passwordInputResult.salt, "salt", ctx); + + const newMasterKey = await this.keyService.makeMasterKey( + passwordInputResult.newPassword, + passwordInputResult.salt, + passwordInputResult.kdfConfig, + ); - [newUserKey, newEncUserKey] = await this.keyService.makeUserKey(newMasterKey); - } else { - [newUserKey, newEncUserKey] = await this.keyService.makeUserKey( - passwordInputResult.newMasterKey, - ); - } + const [newUserKey, newEncUserKey] = await this.keyService.makeUserKey(newMasterKey); if (!newUserKey || !newEncUserKey) { throw new Error("User key could not be created"); @@ -105,7 +61,6 @@ export class DefaultRegistrationFinishService implements RegistrationFinishServi newUserKey, email, passwordInputResult, - newEncUserKey.encryptedString, userAsymmetricKeys, emailVerificationToken, orgSponsoredFreeFamilyPlanToken, @@ -122,7 +77,6 @@ export class DefaultRegistrationFinishService implements RegistrationFinishServi newUserKey: UserKey, email: string, passwordInputResult: PasswordInputResult, - encryptedUserKey: EncryptedString, userAsymmetricKeys: [string, EncString], emailVerificationToken?: string, orgSponsoredFreeFamilyPlanToken?: string, // web only @@ -130,62 +84,38 @@ export class DefaultRegistrationFinishService implements RegistrationFinishServi emergencyAccessId?: string, // web only providerInviteToken?: string, // web only providerUserId?: string, // web only - ): Promise { + ): Promise { const userAsymmetricKeysRequest = new KeysRequest( userAsymmetricKeys[0], userAsymmetricKeys[1].encryptedString, ); - const useNewApi = passwordInputResult.newApisWithInputPasswordFlagEnabled ?? false; - - if (useNewApi) { - // New API path - use new request with new data types - - const masterPasswordAuthentication = - await this.masterPasswordService.makeMasterPasswordAuthenticationData( - passwordInputResult.newPassword, - passwordInputResult.kdfConfig, - passwordInputResult.salt, - ); - - const masterPasswordUnlock = await this.masterPasswordService.makeMasterPasswordUnlockData( + const masterPasswordAuthentication = + await this.masterPasswordService.makeMasterPasswordAuthenticationData( passwordInputResult.newPassword, passwordInputResult.kdfConfig, passwordInputResult.salt, - newUserKey, - ); - - const registerFinishRequest = new RegisterFinishRequestWithAuthUnlockDataTypes( - email, - passwordInputResult.newPasswordHint, - userAsymmetricKeysRequest, - masterPasswordAuthentication, - masterPasswordUnlock, ); - if (emailVerificationToken) { - registerFinishRequest.emailVerificationToken = emailVerificationToken; - } - - return registerFinishRequest; - } else { - // Old API path - use original request with KDF fields - - const registerFinishRequest = new RegisterFinishRequest( - email, - passwordInputResult.newServerMasterKeyHash, - passwordInputResult.newPasswordHint, - encryptedUserKey, - userAsymmetricKeysRequest, - passwordInputResult.kdfConfig.kdfType, - passwordInputResult.kdfConfig.iterations, - ); + const masterPasswordUnlock = await this.masterPasswordService.makeMasterPasswordUnlockData( + passwordInputResult.newPassword, + passwordInputResult.kdfConfig, + passwordInputResult.salt, + newUserKey, + ); - if (emailVerificationToken) { - registerFinishRequest.emailVerificationToken = emailVerificationToken; - } + const registerFinishRequest = new RegisterFinishRequestWithAuthUnlockDataTypes( + email, + passwordInputResult.newPasswordHint, + userAsymmetricKeysRequest, + masterPasswordAuthentication, + masterPasswordUnlock, + ); - return registerFinishRequest; + if (emailVerificationToken) { + registerFinishRequest.emailVerificationToken = emailVerificationToken; } + + return registerFinishRequest; } } diff --git a/libs/common/src/auth/abstractions/account-api.service.ts b/libs/common/src/auth/abstractions/account-api.service.ts index 972c7d848dea..1d82355bf1e9 100644 --- a/libs/common/src/auth/abstractions/account-api.service.ts +++ b/libs/common/src/auth/abstractions/account-api.service.ts @@ -1,5 +1,4 @@ import { RegisterFinishRequestWithAuthUnlockDataTypes } from "../models/request/registration/register-finish-request-with-auth-unlock-data.types"; -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"; import { SetVerifyDevicesRequest } from "../models/request/set-verify-devices.request"; @@ -48,19 +47,9 @@ export abstract class AccountApiService { * @param request - The user's email verification token, the email, newly created user key, and new * asymmetric user key pair along with the KDF information, hashed MP and salt (in the form of the * MasterPasswordAuthenticationData and MasterPasswordUnlockData) used during the process. - * - * OLD DESCRIPTION FOR ORIGINAL API CONTRACT: (the rest of this JSDoc below can be removed in PM-28143) - * - * Completes the registration process. - * - * @param request - The request object containing the user's email verification token, - * the email, hashed MP, newly created user key, and new asymmetric user key pair along - * with the KDF information used during the process. * @returns A promise that resolves when the registration process is successfully completed. */ - abstract registerFinish( - request: RegisterFinishRequest | RegisterFinishRequestWithAuthUnlockDataTypes, - ): Promise; + abstract registerFinish(request: RegisterFinishRequestWithAuthUnlockDataTypes): Promise; /** * Sets the [dbo].[User].[VerifyDevices] flag to true or false. diff --git a/libs/common/src/auth/models/request/registration/register-finish.request.ts b/libs/common/src/auth/models/request/registration/register-finish.request.ts deleted file mode 100644 index b388e8aee497..000000000000 --- a/libs/common/src/auth/models/request/registration/register-finish.request.ts +++ /dev/null @@ -1,34 +0,0 @@ -// 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 { KeysRequest } from "../../../../models/request/keys.request"; - -export class RegisterFinishRequest { - constructor( - public email: string, - - public masterPasswordHash: string, - public masterPasswordHint: string, - - public userSymmetricKey: EncryptedString, - public userAsymmetricKeys: KeysRequest, - - public kdf: KdfType, - public kdfIterations: number, - public kdfMemory?: number, - public kdfParallelism?: number, - - 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, - ) {} -} diff --git a/libs/common/src/auth/services/account-api.service.ts b/libs/common/src/auth/services/account-api.service.ts index b922484b3fd1..f6bb12d0342e 100644 --- a/libs/common/src/auth/services/account-api.service.ts +++ b/libs/common/src/auth/services/account-api.service.ts @@ -8,7 +8,6 @@ import { AccountApiService } from "../abstractions/account-api.service"; import { InternalAccountService } from "../abstractions/account.service"; import { UserVerificationService } from "../abstractions/user-verification/user-verification.service.abstraction"; import { RegisterFinishRequestWithAuthUnlockDataTypes } from "../models/request/registration/register-finish-request-with-auth-unlock-data.types"; -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"; import { SetVerifyDevicesRequest } from "../models/request/set-verify-devices.request"; @@ -85,9 +84,7 @@ export class AccountApiServiceImplementation implements AccountApiService { } } - async registerFinish( - request: RegisterFinishRequest | RegisterFinishRequestWithAuthUnlockDataTypes, - ): Promise { + async registerFinish(request: RegisterFinishRequestWithAuthUnlockDataTypes): Promise { const env = await firstValueFrom(this.environmentService.environment$); try { From a9df65952a0837e473138e79cb63c542f87ee035 Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Fri, 17 Apr 2026 11:20:38 -0700 Subject: [PATCH 2/5] regenerate snapshot tests for web and default RegistrationFinishService --- ...b-registration-finish.service.spec.ts.snap | 176 ++++++++++++++++++ ...t-registration-finish.service.spec.ts.snap | 36 ++++ 2 files changed, 212 insertions(+) create mode 100644 apps/web/src/app/auth/core/services/registration/__snapshots__/web-registration-finish.service.spec.ts.snap create mode 100644 libs/auth/src/angular/registration/registration-finish/__snapshots__/default-registration-finish.service.spec.ts.snap diff --git a/apps/web/src/app/auth/core/services/registration/__snapshots__/web-registration-finish.service.spec.ts.snap b/apps/web/src/app/auth/core/services/registration/__snapshots__/web-registration-finish.service.spec.ts.snap new file mode 100644 index 000000000000..f66ce973be59 --- /dev/null +++ b/apps/web/src/app/auth/core/services/registration/__snapshots__/web-registration-finish.service.spec.ts.snap @@ -0,0 +1,176 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`WebRegistrationFinishService finishRegistration() derives the master key and registers the user with new data types 1`] = ` +RegisterFinishRequestWithAuthUnlockDataTypes { + "acceptEmergencyAccessId": undefined, + "acceptEmergencyAccessInviteToken": undefined, + "email": "test@email.com", + "emailVerificationToken": "emailVerificationToken", + "masterPasswordAuthentication": { + "kdf": PBKDF2KdfConfig { + "iterations": 600000, + "kdfType": 0, + }, + "masterPasswordAuthenticationHash": "authHash", + "salt": "salt", + }, + "masterPasswordHint": "newPasswordHint", + "masterPasswordUnlock": { + "kdf": PBKDF2KdfConfig { + "iterations": 600000, + "kdfType": 0, + }, + "masterKeyWrappedUserKey": "masterKeyWrappedUserKey", + "salt": "salt", + }, + "orgInviteToken": undefined, + "orgSponsoredFreeFamilyPlanToken": undefined, + "organizationUserId": undefined, + "providerInviteToken": undefined, + "providerUserId": undefined, + "userAsymmetricKeys": KeysRequest { + "encryptedPrivateKey": "privateKey", + "publicKey": "publicKey", + }, +} +`; + +exports[`WebRegistrationFinishService finishRegistration() it registers the user with org invite when given an org invite 1`] = ` +RegisterFinishRequestWithAuthUnlockDataTypes { + "acceptEmergencyAccessId": undefined, + "acceptEmergencyAccessInviteToken": undefined, + "email": "test@email.com", + "emailVerificationToken": undefined, + "masterPasswordAuthentication": { + "kdf": PBKDF2KdfConfig { + "iterations": 600000, + "kdfType": 0, + }, + "masterPasswordAuthenticationHash": "authHash", + "salt": "salt", + }, + "masterPasswordHint": "newPasswordHint", + "masterPasswordUnlock": { + "kdf": PBKDF2KdfConfig { + "iterations": 600000, + "kdfType": 0, + }, + "masterKeyWrappedUserKey": "masterKeyWrappedUserKey", + "salt": "salt", + }, + "orgInviteToken": "orgInviteToken", + "orgSponsoredFreeFamilyPlanToken": undefined, + "organizationUserId": "organizationUserId", + "providerInviteToken": undefined, + "providerUserId": undefined, + "userAsymmetricKeys": KeysRequest { + "encryptedPrivateKey": "privateKey", + "publicKey": "publicKey", + }, +} +`; + +exports[`WebRegistrationFinishService finishRegistration() registers the user when given a provider invite token 1`] = ` +RegisterFinishRequestWithAuthUnlockDataTypes { + "acceptEmergencyAccessId": undefined, + "acceptEmergencyAccessInviteToken": undefined, + "email": "test@email.com", + "emailVerificationToken": undefined, + "masterPasswordAuthentication": { + "kdf": PBKDF2KdfConfig { + "iterations": 600000, + "kdfType": 0, + }, + "masterPasswordAuthenticationHash": "authHash", + "salt": "salt", + }, + "masterPasswordHint": "newPasswordHint", + "masterPasswordUnlock": { + "kdf": PBKDF2KdfConfig { + "iterations": 600000, + "kdfType": 0, + }, + "masterKeyWrappedUserKey": "masterKeyWrappedUserKey", + "salt": "salt", + }, + "orgInviteToken": undefined, + "orgSponsoredFreeFamilyPlanToken": undefined, + "organizationUserId": undefined, + "providerInviteToken": "providerInviteToken", + "providerUserId": "providerUserId", + "userAsymmetricKeys": KeysRequest { + "encryptedPrivateKey": "privateKey", + "publicKey": "publicKey", + }, +} +`; + +exports[`WebRegistrationFinishService finishRegistration() registers the user when given an emergency access invite token 1`] = ` +RegisterFinishRequestWithAuthUnlockDataTypes { + "acceptEmergencyAccessId": "emergencyAccessId", + "acceptEmergencyAccessInviteToken": "acceptEmergencyAccessInviteToken", + "email": "test@email.com", + "emailVerificationToken": undefined, + "masterPasswordAuthentication": { + "kdf": PBKDF2KdfConfig { + "iterations": 600000, + "kdfType": 0, + }, + "masterPasswordAuthenticationHash": "authHash", + "salt": "salt", + }, + "masterPasswordHint": "newPasswordHint", + "masterPasswordUnlock": { + "kdf": PBKDF2KdfConfig { + "iterations": 600000, + "kdfType": 0, + }, + "masterKeyWrappedUserKey": "masterKeyWrappedUserKey", + "salt": "salt", + }, + "orgInviteToken": undefined, + "orgSponsoredFreeFamilyPlanToken": undefined, + "organizationUserId": undefined, + "providerInviteToken": undefined, + "providerUserId": undefined, + "userAsymmetricKeys": KeysRequest { + "encryptedPrivateKey": "privateKey", + "publicKey": "publicKey", + }, +} +`; + +exports[`WebRegistrationFinishService finishRegistration() registers the user when given an org sponsored free family plan token 1`] = ` +RegisterFinishRequestWithAuthUnlockDataTypes { + "acceptEmergencyAccessId": undefined, + "acceptEmergencyAccessInviteToken": undefined, + "email": "test@email.com", + "emailVerificationToken": undefined, + "masterPasswordAuthentication": { + "kdf": PBKDF2KdfConfig { + "iterations": 600000, + "kdfType": 0, + }, + "masterPasswordAuthenticationHash": "authHash", + "salt": "salt", + }, + "masterPasswordHint": "newPasswordHint", + "masterPasswordUnlock": { + "kdf": PBKDF2KdfConfig { + "iterations": 600000, + "kdfType": 0, + }, + "masterKeyWrappedUserKey": "masterKeyWrappedUserKey", + "salt": "salt", + }, + "orgInviteToken": undefined, + "orgSponsoredFreeFamilyPlanToken": "orgSponsoredFreeFamilyPlanToken", + "organizationUserId": undefined, + "providerInviteToken": undefined, + "providerUserId": undefined, + "userAsymmetricKeys": KeysRequest { + "encryptedPrivateKey": "privateKey", + "publicKey": "publicKey", + }, +} +`; diff --git a/libs/auth/src/angular/registration/registration-finish/__snapshots__/default-registration-finish.service.spec.ts.snap b/libs/auth/src/angular/registration/registration-finish/__snapshots__/default-registration-finish.service.spec.ts.snap new file mode 100644 index 000000000000..d3caad655daa --- /dev/null +++ b/libs/auth/src/angular/registration/registration-finish/__snapshots__/default-registration-finish.service.spec.ts.snap @@ -0,0 +1,36 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`DefaultRegistrationFinishService finishRegistration() derives the master key and registers the user with new data types 1`] = ` +RegisterFinishRequestWithAuthUnlockDataTypes { + "acceptEmergencyAccessId": undefined, + "acceptEmergencyAccessInviteToken": undefined, + "email": "test@email.com", + "emailVerificationToken": "emailVerificationToken", + "masterPasswordAuthentication": { + "kdf": PBKDF2KdfConfig { + "iterations": 600000, + "kdfType": 0, + }, + "masterPasswordAuthenticationHash": "authHash", + "salt": "test@email.com", + }, + "masterPasswordHint": "newPasswordHint", + "masterPasswordUnlock": { + "kdf": PBKDF2KdfConfig { + "iterations": 600000, + "kdfType": 0, + }, + "masterKeyWrappedUserKey": "wrappedUserKey", + "salt": "test@email.com", + }, + "orgInviteToken": undefined, + "orgSponsoredFreeFamilyPlanToken": undefined, + "organizationUserId": undefined, + "providerInviteToken": undefined, + "providerUserId": undefined, + "userAsymmetricKeys": KeysRequest { + "encryptedPrivateKey": "privateKey", + "publicKey": "publicKey", + }, +} +`; From 775346deaf853012bf61a2ce8d86ac24e038b0af Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Tue, 21 Apr 2026 16:57:36 -0700 Subject: [PATCH 3/5] add test coverage for missing properties on PasswordInputResult --- ...efault-registration-finish.service.spec.ts | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.spec.ts b/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.spec.ts index 211bf19e11af..ca4525d48018 100644 --- a/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.spec.ts +++ b/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.spec.ts @@ -105,6 +105,38 @@ describe("DefaultRegistrationFinishService", () => { masterPasswordService.makeMasterPasswordUnlockData.mockResolvedValue(masterPasswordUnlock); }); + ["newPassword", "salt"].forEach((key) => { + it(`should throw if ${key} is an empty string (falsy) on the PasswordInputResult object`, async () => { + // Arrange + const invalidPasswordInputResult: PasswordInputResult = { + ...passwordInputResult, + [key]: "", + }; + + // Act + const promise = service.finishRegistration(email, invalidPasswordInputResult); + + // Assert + await expect(promise).rejects.toThrow(`${key} is falsy. Could not finish registration.`); + }); + }); + + it("should throw if kdfConfig is undefined on the PasswordInputResult object", async () => { + // Arrange + const invalidPasswordInputResult: PasswordInputResult = { + ...passwordInputResult, + kdfConfig: undefined, + }; + + // Act + const promise = service.finishRegistration(email, invalidPasswordInputResult); + + // Assert + await expect(promise).rejects.toThrow( + "kdfConfig is null or undefined. Could not finish registration.", + ); + }); + it("throws an error if the user key cannot be created", async () => { keyService.makeUserKey.mockResolvedValue([null, null] as any); From 32aa9e43307626a39d691d391a07ab8c1e565bd7 Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Thu, 23 Apr 2026 10:24:02 -0700 Subject: [PATCH 4/5] update tests to remove old/new distinction since the 'new' pattern is now just the only pattern --- .../registration/web-registration-finish.service.spec.ts | 9 +-------- .../default-registration-finish.service.spec.ts | 7 +------ 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.spec.ts b/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.spec.ts index 10d43401dfde..a057366cc2df 100644 --- a/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.spec.ts +++ b/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.spec.ts @@ -244,7 +244,7 @@ describe("WebRegistrationFinishService", () => { ); }); - it("derives the master key and registers the user with new data types", async () => { + it("derives the master key and registers the user", async () => { await service.finishRegistration(email, passwordInputResult, emailVerificationToken); // Verify master key is derived internally @@ -260,16 +260,9 @@ describe("WebRegistrationFinishService", () => { .calls[0][0] as RegisterFinishRequestWithAuthUnlockDataTypes; expect(registerCall).toBeInstanceOf(RegisterFinishRequestWithAuthUnlockDataTypes); - // New API sends structured authentication and unlock data expect(registerCall.masterPasswordAuthentication).toBeDefined(); expect(registerCall.masterPasswordUnlock).toBeDefined(); - // Old API flat fields must NOT be present - expect((registerCall as any).masterPasswordHash).toBeUndefined(); - expect((registerCall as any).userSymmetricKey).toBeUndefined(); - expect((registerCall as any).kdf).toBeUndefined(); - expect((registerCall as any).kdfIterations).toBeUndefined(); - // Unique to this flow: emailVerificationToken is populated expect(registerCall.emailVerificationToken).toEqual(emailVerificationToken); diff --git a/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.spec.ts b/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.spec.ts index ca4525d48018..f58a830818bf 100644 --- a/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.spec.ts +++ b/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.spec.ts @@ -145,7 +145,7 @@ describe("DefaultRegistrationFinishService", () => { ); }); - it("derives the master key and registers the user with new data types", async () => { + it("derives the master key and registers the user", async () => { keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]); keyService.makeKeyPair.mockResolvedValue(userKeyPair); accountApiService.registerFinish.mockResolvedValue(); @@ -182,11 +182,6 @@ describe("DefaultRegistrationFinishService", () => { (registerCall as RegisterFinishRequestWithAuthUnlockDataTypes).masterPasswordUnlock, ).toBeDefined(); - expect((registerCall as any).masterPasswordHash).toBeUndefined(); - expect((registerCall as any).userSymmetricKey).toBeUndefined(); - expect((registerCall as any).kdf).toBeUndefined(); - expect((registerCall as any).kdfIterations).toBeUndefined(); - expect(registerCall).toMatchSnapshot(); }); }); From 930a61aae76c69028964ce8320f93dad7b8f8780 Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Thu, 23 Apr 2026 10:33:34 -0700 Subject: [PATCH 5/5] rename RegisterFinishRequestWithAuthUnlockDataTypes to RegisterFinishRequest since there is now only one model --- ...b-registration-finish.service.spec.ts.snap | 12 +++++----- .../web-registration-finish.service.spec.ts | 22 +++++++++---------- .../web-registration-finish.service.ts | 4 ++-- ...t-registration-finish.service.spec.ts.snap | 4 ++-- ...efault-registration-finish.service.spec.ts | 12 ++++------ .../default-registration-finish.service.ts | 6 ++--- .../auth/abstractions/account-api.service.ts | 4 ++-- ...ta.types.ts => register-finish.request.ts} | 2 +- .../src/auth/services/account-api.service.ts | 4 ++-- 9 files changed, 33 insertions(+), 37 deletions(-) rename libs/common/src/auth/models/request/registration/{register-finish-request-with-auth-unlock-data.types.ts => register-finish.request.ts} (93%) diff --git a/apps/web/src/app/auth/core/services/registration/__snapshots__/web-registration-finish.service.spec.ts.snap b/apps/web/src/app/auth/core/services/registration/__snapshots__/web-registration-finish.service.spec.ts.snap index f66ce973be59..d2c418a3d9de 100644 --- a/apps/web/src/app/auth/core/services/registration/__snapshots__/web-registration-finish.service.spec.ts.snap +++ b/apps/web/src/app/auth/core/services/registration/__snapshots__/web-registration-finish.service.spec.ts.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing -exports[`WebRegistrationFinishService finishRegistration() derives the master key and registers the user with new data types 1`] = ` -RegisterFinishRequestWithAuthUnlockDataTypes { +exports[`WebRegistrationFinishService finishRegistration() derives the master key and registers the user 1`] = ` +RegisterFinishRequest { "acceptEmergencyAccessId": undefined, "acceptEmergencyAccessInviteToken": undefined, "email": "test@email.com", @@ -36,7 +36,7 @@ RegisterFinishRequestWithAuthUnlockDataTypes { `; exports[`WebRegistrationFinishService finishRegistration() it registers the user with org invite when given an org invite 1`] = ` -RegisterFinishRequestWithAuthUnlockDataTypes { +RegisterFinishRequest { "acceptEmergencyAccessId": undefined, "acceptEmergencyAccessInviteToken": undefined, "email": "test@email.com", @@ -71,7 +71,7 @@ RegisterFinishRequestWithAuthUnlockDataTypes { `; exports[`WebRegistrationFinishService finishRegistration() registers the user when given a provider invite token 1`] = ` -RegisterFinishRequestWithAuthUnlockDataTypes { +RegisterFinishRequest { "acceptEmergencyAccessId": undefined, "acceptEmergencyAccessInviteToken": undefined, "email": "test@email.com", @@ -106,7 +106,7 @@ RegisterFinishRequestWithAuthUnlockDataTypes { `; exports[`WebRegistrationFinishService finishRegistration() registers the user when given an emergency access invite token 1`] = ` -RegisterFinishRequestWithAuthUnlockDataTypes { +RegisterFinishRequest { "acceptEmergencyAccessId": "emergencyAccessId", "acceptEmergencyAccessInviteToken": "acceptEmergencyAccessInviteToken", "email": "test@email.com", @@ -141,7 +141,7 @@ RegisterFinishRequestWithAuthUnlockDataTypes { `; exports[`WebRegistrationFinishService finishRegistration() registers the user when given an org sponsored free family plan token 1`] = ` -RegisterFinishRequestWithAuthUnlockDataTypes { +RegisterFinishRequest { "acceptEmergencyAccessId": undefined, "acceptEmergencyAccessInviteToken": undefined, "email": "test@email.com", diff --git a/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.spec.ts b/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.spec.ts index a057366cc2df..7fef7fcfdea8 100644 --- a/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.spec.ts +++ b/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.spec.ts @@ -9,7 +9,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 { RegisterFinishRequestWithAuthUnlockDataTypes } from "@bitwarden/common/auth/models/request/registration/register-finish-request-with-auth-unlock-data.types"; +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 { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; @@ -257,8 +257,8 @@ describe("WebRegistrationFinishService", () => { expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey); const registerCall = accountApiService.registerFinish.mock - .calls[0][0] as RegisterFinishRequestWithAuthUnlockDataTypes; - expect(registerCall).toBeInstanceOf(RegisterFinishRequestWithAuthUnlockDataTypes); + .calls[0][0] as RegisterFinishRequest; + expect(registerCall).toBeInstanceOf(RegisterFinishRequest); expect(registerCall.masterPasswordAuthentication).toBeDefined(); expect(registerCall.masterPasswordUnlock).toBeDefined(); @@ -278,8 +278,8 @@ describe("WebRegistrationFinishService", () => { expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey); const registerCall = accountApiService.registerFinish.mock - .calls[0][0] as RegisterFinishRequestWithAuthUnlockDataTypes; - expect(registerCall).toBeInstanceOf(RegisterFinishRequestWithAuthUnlockDataTypes); + .calls[0][0] as RegisterFinishRequest; + expect(registerCall).toBeInstanceOf(RegisterFinishRequest); expect(registerCall.masterPasswordAuthentication).toBeDefined(); expect(registerCall.masterPasswordUnlock).toBeDefined(); @@ -302,8 +302,8 @@ describe("WebRegistrationFinishService", () => { expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey); const registerCall = accountApiService.registerFinish.mock - .calls[0][0] as RegisterFinishRequestWithAuthUnlockDataTypes; - expect(registerCall).toBeInstanceOf(RegisterFinishRequestWithAuthUnlockDataTypes); + .calls[0][0] as RegisterFinishRequest; + expect(registerCall).toBeInstanceOf(RegisterFinishRequest); expect(registerCall.masterPasswordAuthentication).toBeDefined(); expect(registerCall.masterPasswordUnlock).toBeDefined(); @@ -327,8 +327,8 @@ describe("WebRegistrationFinishService", () => { expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey); const registerCall = accountApiService.registerFinish.mock - .calls[0][0] as RegisterFinishRequestWithAuthUnlockDataTypes; - expect(registerCall).toBeInstanceOf(RegisterFinishRequestWithAuthUnlockDataTypes); + .calls[0][0] as RegisterFinishRequest; + expect(registerCall).toBeInstanceOf(RegisterFinishRequest); expect(registerCall.masterPasswordAuthentication).toBeDefined(); expect(registerCall.masterPasswordUnlock).toBeDefined(); @@ -357,8 +357,8 @@ describe("WebRegistrationFinishService", () => { expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey); const registerCall = accountApiService.registerFinish.mock - .calls[0][0] as RegisterFinishRequestWithAuthUnlockDataTypes; - expect(registerCall).toBeInstanceOf(RegisterFinishRequestWithAuthUnlockDataTypes); + .calls[0][0] as RegisterFinishRequest; + expect(registerCall).toBeInstanceOf(RegisterFinishRequest); expect(registerCall.masterPasswordAuthentication).toBeDefined(); expect(registerCall.masterPasswordUnlock).toBeDefined(); diff --git a/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.ts b/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.ts index 03c14b5412ef..354bf6145487 100644 --- a/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.ts +++ b/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.ts @@ -12,7 +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 { RegisterFinishRequestWithAuthUnlockDataTypes } from "@bitwarden/common/auth/models/request/registration/register-finish-request-with-auth-unlock-data.types"; +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 { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; @@ -88,7 +88,7 @@ export class WebRegistrationFinishService emergencyAccessId?: string, providerInviteToken?: string, providerUserId?: string, - ): Promise { + ): Promise { const registerRequest = await super.buildRegisterRequest( newUserKey, email, diff --git a/libs/auth/src/angular/registration/registration-finish/__snapshots__/default-registration-finish.service.spec.ts.snap b/libs/auth/src/angular/registration/registration-finish/__snapshots__/default-registration-finish.service.spec.ts.snap index d3caad655daa..237f57ca8ad1 100644 --- a/libs/auth/src/angular/registration/registration-finish/__snapshots__/default-registration-finish.service.spec.ts.snap +++ b/libs/auth/src/angular/registration/registration-finish/__snapshots__/default-registration-finish.service.spec.ts.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing -exports[`DefaultRegistrationFinishService finishRegistration() derives the master key and registers the user with new data types 1`] = ` -RegisterFinishRequestWithAuthUnlockDataTypes { +exports[`DefaultRegistrationFinishService finishRegistration() derives the master key and registers the user 1`] = ` +RegisterFinishRequest { "acceptEmergencyAccessId": undefined, "acceptEmergencyAccessInviteToken": undefined, "email": "test@email.com", diff --git a/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.spec.ts b/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.spec.ts index f58a830818bf..4f40d4fa4b43 100644 --- a/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.spec.ts +++ b/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.spec.ts @@ -1,7 +1,7 @@ import { MockProxy, mock } from "jest-mock-extended"; import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-api.service"; -import { RegisterFinishRequestWithAuthUnlockDataTypes } from "@bitwarden/common/auth/models/request/registration/register-finish-request-with-auth-unlock-data.types"; +import { RegisterFinishRequest } from "@bitwarden/common/auth/models/request/registration/register-finish.request"; 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 { @@ -174,13 +174,9 @@ describe("DefaultRegistrationFinishService", () => { ); const registerCall = accountApiService.registerFinish.mock.calls[0][0]; - expect(registerCall).toBeInstanceOf(RegisterFinishRequestWithAuthUnlockDataTypes); - expect( - (registerCall as RegisterFinishRequestWithAuthUnlockDataTypes).masterPasswordAuthentication, - ).toBeDefined(); - expect( - (registerCall as RegisterFinishRequestWithAuthUnlockDataTypes).masterPasswordUnlock, - ).toBeDefined(); + expect(registerCall).toBeInstanceOf(RegisterFinishRequest); + expect((registerCall as RegisterFinishRequest).masterPasswordAuthentication).toBeDefined(); + expect((registerCall as RegisterFinishRequest).masterPasswordUnlock).toBeDefined(); expect(registerCall).toMatchSnapshot(); }); diff --git a/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.ts b/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.ts index 28325d9551ce..94d53035125f 100644 --- a/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.ts +++ b/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.ts @@ -2,7 +2,7 @@ // @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 { RegisterFinishRequestWithAuthUnlockDataTypes } from "@bitwarden/common/auth/models/request/registration/register-finish-request-with-auth-unlock-data.types"; +import { RegisterFinishRequest } from "@bitwarden/common/auth/models/request/registration/register-finish.request"; import { assertNonNullish, assertTruthy } from "@bitwarden/common/auth/utils"; 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"; @@ -84,7 +84,7 @@ export class DefaultRegistrationFinishService implements RegistrationFinishServi emergencyAccessId?: string, // web only providerInviteToken?: string, // web only providerUserId?: string, // web only - ): Promise { + ): Promise { const userAsymmetricKeysRequest = new KeysRequest( userAsymmetricKeys[0], userAsymmetricKeys[1].encryptedString, @@ -104,7 +104,7 @@ export class DefaultRegistrationFinishService implements RegistrationFinishServi newUserKey, ); - const registerFinishRequest = new RegisterFinishRequestWithAuthUnlockDataTypes( + const registerFinishRequest = new RegisterFinishRequest( email, passwordInputResult.newPasswordHint, userAsymmetricKeysRequest, diff --git a/libs/common/src/auth/abstractions/account-api.service.ts b/libs/common/src/auth/abstractions/account-api.service.ts index 1d82355bf1e9..1c38f2676810 100644 --- a/libs/common/src/auth/abstractions/account-api.service.ts +++ b/libs/common/src/auth/abstractions/account-api.service.ts @@ -1,4 +1,4 @@ -import { RegisterFinishRequestWithAuthUnlockDataTypes } from "../models/request/registration/register-finish-request-with-auth-unlock-data.types"; +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"; import { SetVerifyDevicesRequest } from "../models/request/set-verify-devices.request"; @@ -49,7 +49,7 @@ export abstract class AccountApiService { * MasterPasswordAuthenticationData and MasterPasswordUnlockData) used during the process. * @returns A promise that resolves when the registration process is successfully completed. */ - abstract registerFinish(request: RegisterFinishRequestWithAuthUnlockDataTypes): Promise; + abstract registerFinish(request: RegisterFinishRequest): Promise; /** * Sets the [dbo].[User].[VerifyDevices] flag to true or false. diff --git a/libs/common/src/auth/models/request/registration/register-finish-request-with-auth-unlock-data.types.ts b/libs/common/src/auth/models/request/registration/register-finish.request.ts similarity index 93% rename from libs/common/src/auth/models/request/registration/register-finish-request-with-auth-unlock-data.types.ts rename to libs/common/src/auth/models/request/registration/register-finish.request.ts index 7c3adfeb7447..5c2d04a28882 100644 --- a/libs/common/src/auth/models/request/registration/register-finish-request-with-auth-unlock-data.types.ts +++ b/libs/common/src/auth/models/request/registration/register-finish.request.ts @@ -4,7 +4,7 @@ import { } from "../../../../key-management/master-password/types/master-password.types"; import { KeysRequest } from "../../../../models/request/keys.request"; -export class RegisterFinishRequestWithAuthUnlockDataTypes { +export class RegisterFinishRequest { constructor( public email: string, public masterPasswordHint: string, diff --git a/libs/common/src/auth/services/account-api.service.ts b/libs/common/src/auth/services/account-api.service.ts index f6bb12d0342e..923153a0a0b1 100644 --- a/libs/common/src/auth/services/account-api.service.ts +++ b/libs/common/src/auth/services/account-api.service.ts @@ -7,7 +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 { RegisterFinishRequestWithAuthUnlockDataTypes } from "../models/request/registration/register-finish-request-with-auth-unlock-data.types"; +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"; import { SetVerifyDevicesRequest } from "../models/request/set-verify-devices.request"; @@ -84,7 +84,7 @@ export class AccountApiServiceImplementation implements AccountApiService { } } - async registerFinish(request: RegisterFinishRequestWithAuthUnlockDataTypes): Promise { + async registerFinish(request: RegisterFinishRequest): Promise { const env = await firstValueFrom(this.environmentService.environment$); try {