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 ea30e0990ea..9b6c26e0869 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,19 +9,23 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-api.service"; +import { RegisterFinishV2Request } from "@bitwarden/common/auth/models/request/registration/register-finish-v2.request"; +import { RegisterFinishRequest } from "@bitwarden/common/auth/models/request/registration/register-finish.request"; import { OrganizationInvite } from "@bitwarden/common/auth/services/organization-invite/organization-invite"; import { OrganizationInviteService } from "@bitwarden/common/auth/services/organization-invite/organization-invite.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; import { MasterPasswordUnlockData, MasterPasswordSalt, } from "@bitwarden/common/key-management/master-password/types/master-password.types"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { CsprngArray } from "@bitwarden/common/types/csprng"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; -import { DEFAULT_KDF_CONFIG, KeyService } from "@bitwarden/key-management"; +import { DEFAULT_KDF_CONFIG, KeyService, KdfType } from "@bitwarden/key-management"; import { WebRegistrationFinishService } from "./web-registration-finish.service"; @@ -35,6 +39,7 @@ describe("WebRegistrationFinishService", () => { let logService: MockProxy; let policyService: MockProxy; let masterPasswordService: MockProxy; + let configService: MockProxy; beforeEach(() => { keyService = mock(); @@ -43,13 +48,14 @@ describe("WebRegistrationFinishService", () => { policyApiService = mock(); logService = mock(); policyService = mock(); - masterPasswordService = mock(); + configService = mock(); service = new WebRegistrationFinishService( keyService, accountApiService, masterPasswordService, + configService, organizationInviteService, policyApiService, logService, @@ -228,226 +234,296 @@ describe("WebRegistrationFinishService", () => { it("throws an error if the user key cannot be created", async () => { keyService.makeUserKey.mockResolvedValue([null, null]); + configService.getFeatureFlag.mockResolvedValue(false); await expect(service.finishRegistration(email, passwordInputResult)).rejects.toThrow( "User key could not be created", ); }); - it("registers the user when given valid email verification input", async () => { - keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]); - keyService.makeKeyPair.mockResolvedValue(userKeyPair); - accountApiService.registerFinish.mockResolvedValue(); - organizationInviteService.getOrganizationInvite.mockResolvedValue(null); - masterPasswordService.emailToSalt.mockReturnValue(salt); - masterPasswordService.makeMasterPasswordAuthenticationData.mockResolvedValue( - masterPasswordAuthentication, - ); - masterPasswordService.makeMasterPasswordUnlockData.mockResolvedValue(masterPasswordUnlock); + describe("when feature flag is OFF (old API)", () => { + beforeEach(() => { + configService.getFeatureFlag.mockResolvedValue(false); + }); - await service.finishRegistration(email, passwordInputResult, emailVerificationToken); + it("registers the user with KDF fields when given valid email verification input", async () => { + keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]); + keyService.makeKeyPair.mockResolvedValue(userKeyPair); + accountApiService.registerFinish.mockResolvedValue(); + organizationInviteService.getOrganizationInvite.mockResolvedValue(null); - expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey); - expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey); - expect(accountApiService.registerFinish).toHaveBeenCalledWith( - expect.objectContaining({ + await service.finishRegistration(email, passwordInputResult, emailVerificationToken); + + expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey); + expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey); + expect(configService.getFeatureFlag).toHaveBeenCalledWith( + FeatureFlag.PM27044_UpdateRegistrationApis, + ); + expect(accountApiService.registerFinish).toHaveBeenCalledWith( + expect.objectContaining({ + email, + emailVerificationToken: emailVerificationToken, + masterPasswordHash: passwordInputResult.newServerMasterKeyHash, + masterPasswordHint: passwordInputResult.newPasswordHint, + userSymmetricKey: userKeyEncString.encryptedString, + userAsymmetricKeys: { + publicKey: userKeyPair[0], + encryptedPrivateKey: userKeyPair[1].encryptedString, + }, + kdf: KdfType.PBKDF2_SHA256, + kdfIterations: DEFAULT_KDF_CONFIG.iterations, + }), + ); + + // Verify old API fields are present + const registerCall = accountApiService.registerFinish.mock.calls[0][0]; + expect(registerCall).toBeInstanceOf(RegisterFinishRequest); + expect((registerCall as RegisterFinishRequest).kdf).toBeDefined(); + expect((registerCall as RegisterFinishRequest).kdfIterations).toBeDefined(); + }); + + it("it registers the user with org invite when given an org invite", async () => { + keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]); + keyService.makeKeyPair.mockResolvedValue(userKeyPair); + accountApiService.registerFinish.mockResolvedValue(); + organizationInviteService.getOrganizationInvite.mockResolvedValue(orgInvite); + + await service.finishRegistration(email, passwordInputResult); + + expect(accountApiService.registerFinish).toHaveBeenCalledWith( + expect.objectContaining({ + email, + orgInviteToken: orgInvite.token, + organizationUserId: orgInvite.organizationUserId, + kdf: KdfType.PBKDF2_SHA256, + kdfIterations: DEFAULT_KDF_CONFIG.iterations, + }), + ); + }); + + it("registers the user when given an org sponsored free family plan token", async () => { + keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]); + keyService.makeKeyPair.mockResolvedValue(userKeyPair); + accountApiService.registerFinish.mockResolvedValue(); + organizationInviteService.getOrganizationInvite.mockResolvedValue(null); + + await service.finishRegistration( email, - emailVerificationToken: emailVerificationToken, - masterPasswordHash: passwordInputResult.newServerMasterKeyHash, - masterPasswordHint: passwordInputResult.newPasswordHint, - userSymmetricKey: userKeyEncString.encryptedString, - userAsymmetricKeys: { - publicKey: userKeyPair[0], - encryptedPrivateKey: userKeyPair[1].encryptedString, - }, - masterPasswordAuthentication: masterPasswordAuthentication, - masterPasswordUnlock: masterPasswordUnlock, - orgInviteToken: undefined, - organizationUserId: undefined, - orgSponsoredFreeFamilyPlanToken: undefined, - acceptEmergencyAccessInviteToken: undefined, - acceptEmergencyAccessId: undefined, - providerInviteToken: undefined, - providerUserId: undefined, - }), - ); + passwordInputResult, + undefined, + orgSponsoredFreeFamilyPlanToken, + ); + + expect(accountApiService.registerFinish).toHaveBeenCalledWith( + expect.objectContaining({ + email, + orgSponsoredFreeFamilyPlanToken: orgSponsoredFreeFamilyPlanToken, + }), + ); + }); + + it("registers the user when given an emergency access invite token", async () => { + keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]); + keyService.makeKeyPair.mockResolvedValue(userKeyPair); + accountApiService.registerFinish.mockResolvedValue(); + organizationInviteService.getOrganizationInvite.mockResolvedValue(null); + + await service.finishRegistration( + email, + passwordInputResult, + undefined, + undefined, + acceptEmergencyAccessInviteToken, + emergencyAccessId, + ); + + expect(accountApiService.registerFinish).toHaveBeenCalledWith( + expect.objectContaining({ + email, + acceptEmergencyAccessInviteToken: acceptEmergencyAccessInviteToken, + acceptEmergencyAccessId: emergencyAccessId, + }), + ); + }); + + it("registers the user when given a provider invite token", async () => { + keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]); + keyService.makeKeyPair.mockResolvedValue(userKeyPair); + accountApiService.registerFinish.mockResolvedValue(); + organizationInviteService.getOrganizationInvite.mockResolvedValue(null); + + await service.finishRegistration( + email, + passwordInputResult, + undefined, + undefined, + undefined, + undefined, + providerInviteToken, + providerUserId, + ); + + expect(accountApiService.registerFinish).toHaveBeenCalledWith( + expect.objectContaining({ + email, + providerInviteToken: providerInviteToken, + providerUserId: providerUserId, + }), + ); + }); }); - it("it registers the user when given an org invite", async () => { - keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]); - keyService.makeKeyPair.mockResolvedValue(userKeyPair); - accountApiService.registerFinish.mockResolvedValue(); - organizationInviteService.getOrganizationInvite.mockResolvedValue(orgInvite); - masterPasswordService.emailToSalt.mockReturnValue(salt); - masterPasswordService.makeMasterPasswordAuthenticationData.mockResolvedValue( - masterPasswordAuthentication, - ); - masterPasswordService.makeMasterPasswordUnlockData.mockResolvedValue(masterPasswordUnlock); + describe("when feature flag is ON (new API)", () => { + beforeEach(() => { + configService.getFeatureFlag.mockResolvedValue(true); + masterPasswordService.emailToSalt.mockReturnValue(salt); + masterPasswordService.makeMasterPasswordAuthenticationData.mockResolvedValue( + masterPasswordAuthentication, + ); + masterPasswordService.makeMasterPasswordUnlockData.mockResolvedValue(masterPasswordUnlock); + }); - await service.finishRegistration(email, passwordInputResult); + it("registers the user with new data types when given valid email verification input", async () => { + keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]); + keyService.makeKeyPair.mockResolvedValue(userKeyPair); + accountApiService.registerFinish.mockResolvedValue(); + organizationInviteService.getOrganizationInvite.mockResolvedValue(null); - expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey); - expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey); - expect(accountApiService.registerFinish).toHaveBeenCalledWith( - expect.objectContaining({ + await service.finishRegistration(email, passwordInputResult, emailVerificationToken); + + expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey); + expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey); + expect(configService.getFeatureFlag).toHaveBeenCalledWith( + FeatureFlag.PM27044_UpdateRegistrationApis, + ); + expect(accountApiService.registerFinish).toHaveBeenCalledWith( + expect.objectContaining({ + email, + emailVerificationToken: emailVerificationToken, + masterPasswordHash: passwordInputResult.newServerMasterKeyHash, + masterPasswordHint: passwordInputResult.newPasswordHint, + userSymmetricKey: userKeyEncString.encryptedString, + userAsymmetricKeys: { + publicKey: userKeyPair[0], + encryptedPrivateKey: userKeyPair[1].encryptedString, + }, + masterPasswordAuthentication: masterPasswordAuthentication, + masterPasswordUnlock: masterPasswordUnlock, + }), + ); + + // Verify new API fields are present + const registerCall = accountApiService.registerFinish.mock.calls[0][0]; + expect(registerCall).toBeInstanceOf(RegisterFinishV2Request); + expect( + (registerCall as RegisterFinishV2Request).masterPasswordAuthentication, + ).toBeDefined(); + expect((registerCall as RegisterFinishV2Request).masterPasswordUnlock).toBeDefined(); + + // Verify old API fields are NOT present + expect((registerCall as any).kdf).toBeUndefined(); + expect((registerCall as any).kdfIterations).toBeUndefined(); + }); + + it("it registers the user with org invite when given an org invite", async () => { + keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]); + keyService.makeKeyPair.mockResolvedValue(userKeyPair); + accountApiService.registerFinish.mockResolvedValue(); + organizationInviteService.getOrganizationInvite.mockResolvedValue(orgInvite); + + await service.finishRegistration(email, passwordInputResult); + + expect(accountApiService.registerFinish).toHaveBeenCalledWith( + expect.objectContaining({ + email, + orgInviteToken: orgInvite.token, + organizationUserId: orgInvite.organizationUserId, + masterPasswordAuthentication: masterPasswordAuthentication, + masterPasswordUnlock: masterPasswordUnlock, + }), + ); + + // Verify new API fields are present + const registerCall = accountApiService.registerFinish.mock.calls[0][0]; + expect(registerCall).toBeInstanceOf(RegisterFinishV2Request); + }); + + it("registers the user when given an org sponsored free family plan token", async () => { + keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]); + keyService.makeKeyPair.mockResolvedValue(userKeyPair); + accountApiService.registerFinish.mockResolvedValue(); + organizationInviteService.getOrganizationInvite.mockResolvedValue(null); + + await service.finishRegistration( email, - emailVerificationToken: undefined, - masterPasswordHash: passwordInputResult.newServerMasterKeyHash, - masterPasswordHint: passwordInputResult.newPasswordHint, - userSymmetricKey: userKeyEncString.encryptedString, - userAsymmetricKeys: { - publicKey: userKeyPair[0], - encryptedPrivateKey: userKeyPair[1].encryptedString, - }, - masterPasswordAuthentication: masterPasswordAuthentication, - masterPasswordUnlock: masterPasswordUnlock, - orgInviteToken: orgInvite.token, - organizationUserId: orgInvite.organizationUserId, - orgSponsoredFreeFamilyPlanToken: undefined, - acceptEmergencyAccessInviteToken: undefined, - acceptEmergencyAccessId: undefined, - providerInviteToken: undefined, - providerUserId: undefined, - }), - ); - }); + passwordInputResult, + undefined, + orgSponsoredFreeFamilyPlanToken, + ); - it("registers the user when given an org sponsored free family plan token", async () => { - keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]); - keyService.makeKeyPair.mockResolvedValue(userKeyPair); - accountApiService.registerFinish.mockResolvedValue(); - organizationInviteService.getOrganizationInvite.mockResolvedValue(null); - masterPasswordService.emailToSalt.mockReturnValue(salt); - masterPasswordService.makeMasterPasswordAuthenticationData.mockResolvedValue( - masterPasswordAuthentication, - ); - masterPasswordService.makeMasterPasswordUnlockData.mockResolvedValue(masterPasswordUnlock); + expect(accountApiService.registerFinish).toHaveBeenCalledWith( + expect.objectContaining({ + email, + orgSponsoredFreeFamilyPlanToken: orgSponsoredFreeFamilyPlanToken, + masterPasswordAuthentication: masterPasswordAuthentication, + masterPasswordUnlock: masterPasswordUnlock, + }), + ); + }); - await service.finishRegistration( - email, - passwordInputResult, - undefined, - orgSponsoredFreeFamilyPlanToken, - ); + it("registers the user when given an emergency access invite token", async () => { + keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]); + keyService.makeKeyPair.mockResolvedValue(userKeyPair); + accountApiService.registerFinish.mockResolvedValue(); + organizationInviteService.getOrganizationInvite.mockResolvedValue(null); - expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey); - expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey); - expect(accountApiService.registerFinish).toHaveBeenCalledWith( - expect.objectContaining({ + await service.finishRegistration( email, - emailVerificationToken: undefined, - masterPasswordHash: passwordInputResult.newServerMasterKeyHash, - masterPasswordHint: passwordInputResult.newPasswordHint, - userSymmetricKey: userKeyEncString.encryptedString, - userAsymmetricKeys: { - publicKey: userKeyPair[0], - encryptedPrivateKey: userKeyPair[1].encryptedString, - }, - masterPasswordAuthentication: masterPasswordAuthentication, - masterPasswordUnlock: masterPasswordUnlock, - orgInviteToken: undefined, - organizationUserId: undefined, - orgSponsoredFreeFamilyPlanToken: orgSponsoredFreeFamilyPlanToken, - acceptEmergencyAccessInviteToken: undefined, - acceptEmergencyAccessId: undefined, - providerInviteToken: undefined, - providerUserId: undefined, - }), - ); - }); + passwordInputResult, + undefined, + undefined, + acceptEmergencyAccessInviteToken, + emergencyAccessId, + ); - it("registers the user when given an emergency access invite token", async () => { - keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]); - keyService.makeKeyPair.mockResolvedValue(userKeyPair); - accountApiService.registerFinish.mockResolvedValue(); - organizationInviteService.getOrganizationInvite.mockResolvedValue(null); - masterPasswordService.emailToSalt.mockReturnValue(salt); - masterPasswordService.makeMasterPasswordAuthenticationData.mockResolvedValue( - masterPasswordAuthentication, - ); - masterPasswordService.makeMasterPasswordUnlockData.mockResolvedValue(masterPasswordUnlock); + expect(accountApiService.registerFinish).toHaveBeenCalledWith( + expect.objectContaining({ + email, + acceptEmergencyAccessInviteToken: acceptEmergencyAccessInviteToken, + acceptEmergencyAccessId: emergencyAccessId, + masterPasswordAuthentication: masterPasswordAuthentication, + masterPasswordUnlock: masterPasswordUnlock, + }), + ); + }); - await service.finishRegistration( - email, - passwordInputResult, - undefined, - undefined, - acceptEmergencyAccessInviteToken, - emergencyAccessId, - ); + it("registers the user when given a provider invite token", async () => { + keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]); + keyService.makeKeyPair.mockResolvedValue(userKeyPair); + accountApiService.registerFinish.mockResolvedValue(); + organizationInviteService.getOrganizationInvite.mockResolvedValue(null); - expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey); - expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey); - expect(accountApiService.registerFinish).toHaveBeenCalledWith( - expect.objectContaining({ + await service.finishRegistration( email, - emailVerificationToken: undefined, - masterPasswordHash: passwordInputResult.newServerMasterKeyHash, - masterPasswordHint: passwordInputResult.newPasswordHint, - userSymmetricKey: userKeyEncString.encryptedString, - userAsymmetricKeys: { - publicKey: userKeyPair[0], - encryptedPrivateKey: userKeyPair[1].encryptedString, - }, - masterPasswordAuthentication: masterPasswordAuthentication, - masterPasswordUnlock: masterPasswordUnlock, - orgInviteToken: undefined, - organizationUserId: undefined, - orgSponsoredFreeFamilyPlanToken: undefined, - acceptEmergencyAccessInviteToken: acceptEmergencyAccessInviteToken, - acceptEmergencyAccessId: emergencyAccessId, - providerInviteToken: undefined, - providerUserId: undefined, - }), - ); - }); + passwordInputResult, + undefined, + undefined, + undefined, + undefined, + providerInviteToken, + providerUserId, + ); - it("registers the user when given a provider invite token", async () => { - keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]); - keyService.makeKeyPair.mockResolvedValue(userKeyPair); - accountApiService.registerFinish.mockResolvedValue(); - organizationInviteService.getOrganizationInvite.mockResolvedValue(null); - masterPasswordService.emailToSalt.mockReturnValue(salt); - masterPasswordService.makeMasterPasswordAuthenticationData.mockResolvedValue( - masterPasswordAuthentication, - ); - masterPasswordService.makeMasterPasswordUnlockData.mockResolvedValue(masterPasswordUnlock); - - await service.finishRegistration( - email, - passwordInputResult, - undefined, - undefined, - undefined, - undefined, - providerInviteToken, - providerUserId, - ); - - expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey); - expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey); - expect(accountApiService.registerFinish).toHaveBeenCalledWith( - expect.objectContaining({ - email, - emailVerificationToken: undefined, - masterPasswordHash: passwordInputResult.newServerMasterKeyHash, - masterPasswordHint: passwordInputResult.newPasswordHint, - userSymmetricKey: userKeyEncString.encryptedString, - userAsymmetricKeys: { - publicKey: userKeyPair[0], - encryptedPrivateKey: userKeyPair[1].encryptedString, - }, - masterPasswordAuthentication: masterPasswordAuthentication, - masterPasswordUnlock: masterPasswordUnlock, - orgInviteToken: undefined, - organizationUserId: undefined, - orgSponsoredFreeFamilyPlanToken: undefined, - acceptEmergencyAccessInviteToken: undefined, - acceptEmergencyAccessId: undefined, - providerInviteToken: providerInviteToken, - providerUserId: providerUserId, - }), - ); + expect(accountApiService.registerFinish).toHaveBeenCalledWith( + expect.objectContaining({ + email, + providerInviteToken: providerInviteToken, + providerUserId: providerUserId, + masterPasswordAuthentication: masterPasswordAuthentication, + masterPasswordUnlock: masterPasswordUnlock, + }), + ); + }); }); }); }); 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 dc2f4596521..77c01e9eb76 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,6 +12,7 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-api.service"; +import { RegisterFinishV2Request } from "@bitwarden/common/auth/models/request/registration/register-finish-v2.request"; import { RegisterFinishRequest } from "@bitwarden/common/auth/models/request/registration/register-finish.request"; import { OrganizationInviteService } from "@bitwarden/common/auth/services/organization-invite/organization-invite.service"; import { @@ -19,6 +20,7 @@ import { EncString, } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { UserKey } from "@bitwarden/common/types/key"; import { KeyService } from "@bitwarden/key-management"; @@ -31,12 +33,13 @@ export class WebRegistrationFinishService protected keyService: KeyService, protected accountApiService: AccountApiService, protected masterPasswordService: MasterPasswordServiceAbstraction, + protected configService: ConfigService, private organizationInviteService: OrganizationInviteService, private policyApiService: PolicyApiServiceAbstraction, private logService: LogService, private policyService: PolicyService, ) { - super(keyService, accountApiService, masterPasswordService); + super(keyService, accountApiService, masterPasswordService, configService); } override async getOrgNameFromOrgInvite(): Promise { @@ -92,7 +95,7 @@ export class WebRegistrationFinishService emergencyAccessId?: string, providerInviteToken?: string, providerUserId?: string, - ): Promise { + ): Promise { const registerRequest = await super.buildRegisterRequest( newUserKey, email, diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index 2f672ce614c..2ce0c1dc6eb 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -290,6 +290,7 @@ 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 442c5ceca92..853d979d340 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -1598,7 +1598,12 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: RegistrationFinishServiceAbstraction, useClass: DefaultRegistrationFinishService, - deps: [KeyService, AccountApiServiceAbstraction, MasterPasswordServiceAbstraction], + deps: [ + KeyService, + AccountApiServiceAbstraction, + MasterPasswordServiceAbstraction, + ConfigService, + ], }), safeProvider({ provide: TwoFactorAuthComponentService, 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 9df48f2f6a7..3cefc8613de 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,6 +1,9 @@ import { MockProxy, mock } from "jest-mock-extended"; import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-api.service"; +import { RegisterFinishV2Request } from "@bitwarden/common/auth/models/request/registration/register-finish-v2.request"; +import { RegisterFinishRequest } from "@bitwarden/common/auth/models/request/registration/register-finish.request"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; import { @@ -10,10 +13,11 @@ import { MasterPasswordSalt, MasterKeyWrappedUserKey, } from "@bitwarden/common/key-management/master-password/types/master-password.types"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { CsprngArray } from "@bitwarden/common/types/csprng"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; -import { DEFAULT_KDF_CONFIG, KeyService } from "@bitwarden/key-management"; +import { DEFAULT_KDF_CONFIG, KeyService, KdfType } from "@bitwarden/key-management"; import { PasswordInputResult } from "../../input-password/password-input-result"; @@ -25,16 +29,19 @@ describe("DefaultRegistrationFinishService", () => { let keyService: MockProxy; 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, ); }); @@ -89,60 +96,126 @@ describe("DefaultRegistrationFinishService", () => { it("throws an error if the user key cannot be created", async () => { keyService.makeUserKey.mockResolvedValue([null, null]); masterPasswordService.emailToSalt.mockReturnValue("salt" as MasterPasswordSalt); + configService.getFeatureFlag.mockResolvedValue(false); await expect(service.finishRegistration(email, passwordInputResult)).rejects.toThrow( "User key could not be created", ); }); - it("registers the user when given valid email verification input", async () => { - keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]); - keyService.makeKeyPair.mockResolvedValue(userKeyPair); - accountApiService.registerFinish.mockResolvedValue(); - const salt = "salt" as MasterPasswordSalt; - const masterPasswordAuthentication: MasterPasswordAuthenticationData = { - salt, - kdf: DEFAULT_KDF_CONFIG, - masterPasswordAuthenticationHash: "authHash" as MasterPasswordAuthenticationHash, - }; - const masterPasswordUnlock = new MasterPasswordUnlockData( - salt, - DEFAULT_KDF_CONFIG, - "wrappedUserKey" as MasterKeyWrappedUserKey, - ); - masterPasswordService.emailToSalt.mockReturnValue(salt); - masterPasswordService.makeMasterPasswordAuthenticationData.mockResolvedValue( - masterPasswordAuthentication, - ); - masterPasswordService.makeMasterPasswordUnlockData.mockResolvedValue(masterPasswordUnlock); + describe("when feature flag is OFF (old API)", () => { + beforeEach(() => { + configService.getFeatureFlag.mockResolvedValue(false); + }); - await service.finishRegistration(email, passwordInputResult, emailVerificationToken); + it("registers the user with KDF fields when given valid email verification input", async () => { + keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]); + keyService.makeKeyPair.mockResolvedValue(userKeyPair); + accountApiService.registerFinish.mockResolvedValue(); - expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey); - expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey); - expect(accountApiService.registerFinish).toHaveBeenCalledWith( - expect.objectContaining({ - email, - emailVerificationToken: emailVerificationToken, - masterPasswordHash: passwordInputResult.newServerMasterKeyHash, - masterPasswordHint: passwordInputResult.newPasswordHint, - userSymmetricKey: userKeyEncString.encryptedString, - userAsymmetricKeys: { - publicKey: userKeyPair[0], - encryptedPrivateKey: userKeyPair[1].encryptedString, - }, - masterPasswordAuthentication: masterPasswordAuthentication, - masterPasswordUnlock: masterPasswordUnlock, - // Web only fields should be undefined - orgInviteToken: undefined, - organizationUserId: undefined, - orgSponsoredFreeFamilyPlanToken: undefined, - acceptEmergencyAccessInviteToken: undefined, - acceptEmergencyAccessId: undefined, - providerInviteToken: undefined, - providerUserId: undefined, - }), - ); + await service.finishRegistration(email, passwordInputResult, emailVerificationToken); + + expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey); + expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey); + expect(configService.getFeatureFlag).toHaveBeenCalledWith( + FeatureFlag.PM27044_UpdateRegistrationApis, + ); + expect(accountApiService.registerFinish).toHaveBeenCalledWith( + expect.objectContaining({ + email, + emailVerificationToken: emailVerificationToken, + masterPasswordHash: passwordInputResult.newServerMasterKeyHash, + masterPasswordHint: passwordInputResult.newPasswordHint, + userSymmetricKey: userKeyEncString.encryptedString, + userAsymmetricKeys: { + publicKey: userKeyPair[0], + encryptedPrivateKey: userKeyPair[1].encryptedString, + }, + kdf: KdfType.PBKDF2_SHA256, + kdfIterations: DEFAULT_KDF_CONFIG.iterations, + kdfMemory: undefined, + kdfParallelism: undefined, + }), + ); + + // Verify old API fields are present + const registerCall = accountApiService.registerFinish.mock.calls[0][0]; + expect(registerCall).toBeInstanceOf(RegisterFinishRequest); + expect((registerCall as RegisterFinishRequest).kdf).toBeDefined(); + expect((registerCall as RegisterFinishRequest).kdfIterations).toBeDefined(); + + // Verify new API fields are NOT present + expect((registerCall as any).masterPasswordAuthentication).toBeUndefined(); + expect((registerCall as any).masterPasswordUnlock).toBeUndefined(); + }); + }); + + describe("when feature flag is ON (new API)", () => { + let salt: MasterPasswordSalt; + let masterPasswordAuthentication: MasterPasswordAuthenticationData; + let masterPasswordUnlock: MasterPasswordUnlockData; + + beforeEach(() => { + configService.getFeatureFlag.mockResolvedValue(true); + + salt = "salt" as MasterPasswordSalt; + masterPasswordAuthentication = { + salt, + kdf: DEFAULT_KDF_CONFIG, + masterPasswordAuthenticationHash: "authHash" as MasterPasswordAuthenticationHash, + }; + masterPasswordUnlock = new MasterPasswordUnlockData( + salt, + DEFAULT_KDF_CONFIG, + "wrappedUserKey" as MasterKeyWrappedUserKey, + ); + masterPasswordService.emailToSalt.mockReturnValue(salt); + masterPasswordService.makeMasterPasswordAuthenticationData.mockResolvedValue( + masterPasswordAuthentication, + ); + masterPasswordService.makeMasterPasswordUnlockData.mockResolvedValue(masterPasswordUnlock); + }); + + it("registers the user with new data types when given valid email verification input", async () => { + keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]); + keyService.makeKeyPair.mockResolvedValue(userKeyPair); + accountApiService.registerFinish.mockResolvedValue(); + + await service.finishRegistration(email, passwordInputResult, emailVerificationToken); + + expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey); + expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey); + expect(configService.getFeatureFlag).toHaveBeenCalledWith( + FeatureFlag.PM27044_UpdateRegistrationApis, + ); + expect(accountApiService.registerFinish).toHaveBeenCalledWith( + expect.objectContaining({ + email, + emailVerificationToken: emailVerificationToken, + masterPasswordHash: passwordInputResult.newServerMasterKeyHash, + masterPasswordHint: passwordInputResult.newPasswordHint, + userSymmetricKey: userKeyEncString.encryptedString, + userAsymmetricKeys: { + publicKey: userKeyPair[0], + encryptedPrivateKey: userKeyPair[1].encryptedString, + }, + masterPasswordAuthentication: masterPasswordAuthentication, + masterPasswordUnlock: masterPasswordUnlock, + }), + ); + + // Verify new API fields are present + const registerCall = accountApiService.registerFinish.mock.calls[0][0]; + expect(registerCall).toBeInstanceOf(RegisterFinishV2Request); + expect( + (registerCall as RegisterFinishV2Request).masterPasswordAuthentication, + ).toBeDefined(); + expect((registerCall as RegisterFinishV2Request).masterPasswordUnlock).toBeDefined(); + + // Verify old API fields are NOT present + expect((registerCall as any).kdf).toBeUndefined(); + expect((registerCall as any).kdfIterations).toBeUndefined(); + }); }); }); }); 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 132048f838d..edd03f525db 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,15 +2,18 @@ // @ts-strict-ignore import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-api.service"; +import { RegisterFinishV2Request } from "@bitwarden/common/auth/models/request/registration/register-finish-v2.request"; import { RegisterFinishRequest } from "@bitwarden/common/auth/models/request/registration/register-finish.request"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { EncryptedString, EncString, } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; import { KeysRequest } from "@bitwarden/common/models/request/keys.request"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { UserKey } from "@bitwarden/common/types/key"; -import { KeyService } from "@bitwarden/key-management"; +import { Argon2KdfConfig, KeyService, KdfType } from "@bitwarden/key-management"; import { PasswordInputResult } from "../../input-password/password-input-result"; @@ -21,6 +24,7 @@ export class DefaultRegistrationFinishService implements RegistrationFinishServi protected keyService: KeyService, protected accountApiService: AccountApiService, protected masterPasswordService: MasterPasswordServiceAbstraction, + protected configService: ConfigService, ) {} getOrgNameFromOrgInvite(): Promise { @@ -79,44 +83,72 @@ 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, ); - // Get salt value, for now we derive it from the email but this could change to be random bytes - // in the future once the email and salt are separated. - const salt = this.masterPasswordService.emailToSalt(email); + const useNewApi = await this.configService.getFeatureFlag( + FeatureFlag.PM27044_UpdateRegistrationApis, + ); - const masterPasswordAuthentication = - await this.masterPasswordService.makeMasterPasswordAuthenticationData( + if (useNewApi) { + // New API path - use V2 request with new data types + const salt = this.masterPasswordService.emailToSalt(email); + + const masterPasswordAuthentication = + await this.masterPasswordService.makeMasterPasswordAuthenticationData( + passwordInputResult.newPassword, + passwordInputResult.kdfConfig, + salt, + ); + + const masterPasswordUnlock = await this.masterPasswordService.makeMasterPasswordUnlockData( passwordInputResult.newPassword, passwordInputResult.kdfConfig, salt, + newUserKey, ); - const masterPasswordUnlock = await this.masterPasswordService.makeMasterPasswordUnlockData( - passwordInputResult.newPassword, - passwordInputResult.kdfConfig, - salt, - newUserKey, - ); + const registerFinishRequest = new RegisterFinishV2Request( + email, + passwordInputResult.newServerMasterKeyHash, + passwordInputResult.newPasswordHint, + encryptedUserKey, + userAsymmetricKeysRequest, + masterPasswordAuthentication, + masterPasswordUnlock, + ); - const registerFinishRequest = new RegisterFinishRequest( - email, - passwordInputResult.newServerMasterKeyHash, - passwordInputResult.newPasswordHint, - encryptedUserKey, - userAsymmetricKeysRequest, - masterPasswordAuthentication, - masterPasswordUnlock, - ); + if (emailVerificationToken) { + registerFinishRequest.emailVerificationToken = emailVerificationToken; + } - if (emailVerificationToken) { - registerFinishRequest.emailVerificationToken = emailVerificationToken; + return registerFinishRequest; + } else { + // Old API path - use original request with KDF fields + const kdfConfig = passwordInputResult.kdfConfig; + + const registerFinishRequest = new RegisterFinishRequest( + email, + passwordInputResult.newServerMasterKeyHash, + passwordInputResult.newPasswordHint, + encryptedUserKey, + userAsymmetricKeysRequest, + kdfConfig.kdfType, + kdfConfig.iterations, + kdfConfig.kdfType === KdfType.Argon2id ? (kdfConfig as Argon2KdfConfig).memory : undefined, + kdfConfig.kdfType === KdfType.Argon2id + ? (kdfConfig as Argon2KdfConfig).parallelism + : undefined, + ); + + if (emailVerificationToken) { + registerFinishRequest.emailVerificationToken = emailVerificationToken; + } + + return registerFinishRequest; } - - return registerFinishRequest; } } diff --git a/libs/common/src/auth/abstractions/account-api.service.ts b/libs/common/src/auth/abstractions/account-api.service.ts index f1773e33e6a..d010fdfc124 100644 --- a/libs/common/src/auth/abstractions/account-api.service.ts +++ b/libs/common/src/auth/abstractions/account-api.service.ts @@ -1,3 +1,4 @@ +import { RegisterFinishV2Request } from "../models/request/registration/register-finish-v2.request"; import { RegisterFinishRequest } from "../models/request/registration/register-finish.request"; import { RegisterSendVerificationEmailRequest } from "../models/request/registration/register-send-verification-email.request"; import { RegisterVerificationEmailClickedRequest } from "../models/request/registration/register-verification-email-clicked.request"; @@ -49,7 +50,7 @@ export abstract class AccountApiService { * with the KDF information used during the process. * @returns A promise that resolves when the registration process is successfully completed. */ - abstract registerFinish(request: RegisterFinishRequest): Promise; + abstract registerFinish(request: RegisterFinishRequest | RegisterFinishV2Request): Promise; /** * Sets the [dbo].[User].[VerifyDevices] flag to true or false. diff --git a/libs/common/src/auth/models/request/registration/register-finish-v2.request.ts b/libs/common/src/auth/models/request/registration/register-finish-v2.request.ts new file mode 100644 index 00000000000..cab57023b71 --- /dev/null +++ b/libs/common/src/auth/models/request/registration/register-finish-v2.request.ts @@ -0,0 +1,32 @@ +import { EncryptedString } from "../../../../key-management/crypto/models/enc-string"; +import { + MasterPasswordAuthenticationData, + MasterPasswordUnlockData, +} from "../../../../key-management/master-password/types/master-password.types"; +import { KeysRequest } from "../../../../models/request/keys.request"; + +export class RegisterFinishV2Request { + constructor( + public email: string, + + public masterPasswordHash: string, + public masterPasswordHint: string, + + public userSymmetricKey: EncryptedString, + public userAsymmetricKeys: KeysRequest, + + public masterPasswordAuthentication: MasterPasswordAuthenticationData, + public masterPasswordUnlock: MasterPasswordUnlockData, + + public emailVerificationToken?: string, + public orgSponsoredFreeFamilyPlanToken?: string, + public acceptEmergencyAccessInviteToken?: string, + public acceptEmergencyAccessId?: string, + public providerInviteToken?: string, + public providerUserId?: string, + + // Org Invite data (only applies on web) + public organizationUserId?: string, + public orgInviteToken?: string, + ) {} +} 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 index c76008b8108..b388e8aee49 100644 --- a/libs/common/src/auth/models/request/registration/register-finish.request.ts +++ b/libs/common/src/auth/models/request/registration/register-finish.request.ts @@ -1,8 +1,8 @@ +// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. +// eslint-disable-next-line no-restricted-imports +import { KdfType } from "@bitwarden/key-management"; + import { EncryptedString } from "../../../../key-management/crypto/models/enc-string"; -import { - MasterPasswordAuthenticationData, - MasterPasswordUnlockData, -} from "../../../../key-management/master-password/types/master-password.types"; import { KeysRequest } from "../../../../models/request/keys.request"; export class RegisterFinishRequest { @@ -15,8 +15,10 @@ export class RegisterFinishRequest { public userSymmetricKey: EncryptedString, public userAsymmetricKeys: KeysRequest, - public masterPasswordAuthentication: MasterPasswordAuthenticationData, - public masterPasswordUnlock: MasterPasswordUnlockData, + public kdf: KdfType, + public kdfIterations: number, + public kdfMemory?: number, + public kdfParallelism?: number, public emailVerificationToken?: string, public orgSponsoredFreeFamilyPlanToken?: string, diff --git a/libs/common/src/auth/services/account-api.service.ts b/libs/common/src/auth/services/account-api.service.ts index 923153a0a0b..6e3bf84a504 100644 --- a/libs/common/src/auth/services/account-api.service.ts +++ b/libs/common/src/auth/services/account-api.service.ts @@ -7,6 +7,7 @@ import { LogService } from "../../platform/abstractions/log.service"; import { AccountApiService } from "../abstractions/account-api.service"; import { InternalAccountService } from "../abstractions/account.service"; import { UserVerificationService } from "../abstractions/user-verification/user-verification.service.abstraction"; +import { RegisterFinishV2Request } from "../models/request/registration/register-finish-v2.request"; import { RegisterFinishRequest } from "../models/request/registration/register-finish.request"; import { RegisterSendVerificationEmailRequest } from "../models/request/registration/register-send-verification-email.request"; import { RegisterVerificationEmailClickedRequest } from "../models/request/registration/register-verification-email-clicked.request"; @@ -84,7 +85,7 @@ export class AccountApiServiceImplementation implements AccountApiService { } } - async registerFinish(request: RegisterFinishRequest): Promise { + async registerFinish(request: RegisterFinishRequest | RegisterFinishV2Request): Promise { const env = await firstValueFrom(this.environmentService.environment$); try { diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 0f834abbe2a..7c07a39e0ab 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -46,6 +46,7 @@ export enum FeatureFlag { ConsolidatedSessionTimeoutComponent = "pm-26056-consolidated-session-timeout-component", PM27279_V2RegistrationTdeJit = "pm-27279-v2-registration-tde-jit", EnableAccountEncryptionV2KeyConnectorRegistration = "enable-account-encryption-v2-key-connector-registration", + PM27044_UpdateRegistrationApis = "pm-27044-update-registration-apis", /* Tools */ UseSdkPasswordGenerators = "pm-19976-use-sdk-password-generators", @@ -153,6 +154,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.ConsolidatedSessionTimeoutComponent]: FALSE, [FeatureFlag.PM27279_V2RegistrationTdeJit]: FALSE, [FeatureFlag.EnableAccountEncryptionV2KeyConnectorRegistration]: FALSE, + [FeatureFlag.PM27044_UpdateRegistrationApis]: FALSE, /* Platform */ [FeatureFlag.IpcChannelFramework]: FALSE,