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 dc8dfcfb51..1c1e970b7e 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 @@ -20,7 +20,7 @@ import { OrganizationInvite } from "../../../organization-invite/organization-in import { WebRegistrationFinishService } from "./web-registration-finish.service"; -describe("DefaultRegistrationFinishService", () => { +describe("WebRegistrationFinishService", () => { let service: WebRegistrationFinishService; let keyService: MockProxy; @@ -167,6 +167,11 @@ describe("DefaultRegistrationFinishService", () => { let capchaBypassToken: string; let orgInvite: OrganizationInvite; + let orgSponsoredFreeFamilyPlanToken: string; + let acceptEmergencyAccessInviteToken: string; + let emergencyAccessId: string; + let providerInviteToken: string; + let providerUserId: string; beforeEach(() => { email = "test@email.com"; @@ -190,6 +195,12 @@ describe("DefaultRegistrationFinishService", () => { orgInvite = new OrganizationInvite(); orgInvite.organizationUserId = "organizationUserId"; orgInvite.token = "orgInviteToken"; + + orgSponsoredFreeFamilyPlanToken = "orgSponsoredFreeFamilyPlanToken"; + acceptEmergencyAccessInviteToken = "acceptEmergencyAccessInviteToken"; + emergencyAccessId = "emergencyAccessId"; + providerInviteToken = "providerInviteToken"; + providerUserId = "providerUserId"; }); it("throws an error if the user key cannot be created", async () => { @@ -233,6 +244,11 @@ describe("DefaultRegistrationFinishService", () => { kdfParallelism: undefined, orgInviteToken: undefined, organizationUserId: undefined, + orgSponsoredFreeFamilyPlanToken: undefined, + acceptEmergencyAccessInviteToken: undefined, + acceptEmergencyAccessId: undefined, + providerInviteToken: undefined, + providerUserId: undefined, }), ); }); @@ -266,6 +282,146 @@ describe("DefaultRegistrationFinishService", () => { kdfParallelism: undefined, orgInviteToken: orgInvite.token, organizationUserId: orgInvite.organizationUserId, + orgSponsoredFreeFamilyPlanToken: undefined, + acceptEmergencyAccessInviteToken: undefined, + acceptEmergencyAccessId: undefined, + providerInviteToken: undefined, + providerUserId: undefined, + }), + ); + }); + + it("registers the user and returns a captcha bypass token when given an org sponsored free family plan token", async () => { + keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]); + keyService.makeKeyPair.mockResolvedValue(userKeyPair); + accountApiService.registerFinish.mockResolvedValue(capchaBypassToken); + acceptOrgInviteService.getOrganizationInvite.mockResolvedValue(null); + + const result = await service.finishRegistration( + email, + passwordInputResult, + undefined, + orgSponsoredFreeFamilyPlanToken, + ); + + expect(result).toEqual(capchaBypassToken); + + expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey); + expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey); + expect(accountApiService.registerFinish).toHaveBeenCalledWith( + expect.objectContaining({ + email, + emailVerificationToken: undefined, + masterPasswordHash: passwordInputResult.masterKeyHash, + masterPasswordHint: passwordInputResult.hint, + userSymmetricKey: userKeyEncString.encryptedString, + userAsymmetricKeys: { + publicKey: userKeyPair[0], + encryptedPrivateKey: userKeyPair[1].encryptedString, + }, + kdf: passwordInputResult.kdfConfig.kdfType, + kdfIterations: passwordInputResult.kdfConfig.iterations, + kdfMemory: undefined, + kdfParallelism: undefined, + orgInviteToken: undefined, + organizationUserId: undefined, + orgSponsoredFreeFamilyPlanToken: orgSponsoredFreeFamilyPlanToken, + acceptEmergencyAccessInviteToken: undefined, + acceptEmergencyAccessId: undefined, + providerInviteToken: undefined, + providerUserId: undefined, + }), + ); + }); + + it("registers the user and returns a captcha bypass token when given an emergency access invite token", async () => { + keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]); + keyService.makeKeyPair.mockResolvedValue(userKeyPair); + accountApiService.registerFinish.mockResolvedValue(capchaBypassToken); + acceptOrgInviteService.getOrganizationInvite.mockResolvedValue(null); + + const result = await service.finishRegistration( + email, + passwordInputResult, + undefined, + undefined, + acceptEmergencyAccessInviteToken, + emergencyAccessId, + ); + + expect(result).toEqual(capchaBypassToken); + + expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey); + expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey); + expect(accountApiService.registerFinish).toHaveBeenCalledWith( + expect.objectContaining({ + email, + emailVerificationToken: undefined, + masterPasswordHash: passwordInputResult.masterKeyHash, + masterPasswordHint: passwordInputResult.hint, + userSymmetricKey: userKeyEncString.encryptedString, + userAsymmetricKeys: { + publicKey: userKeyPair[0], + encryptedPrivateKey: userKeyPair[1].encryptedString, + }, + kdf: passwordInputResult.kdfConfig.kdfType, + kdfIterations: passwordInputResult.kdfConfig.iterations, + kdfMemory: undefined, + kdfParallelism: undefined, + orgInviteToken: undefined, + organizationUserId: undefined, + orgSponsoredFreeFamilyPlanToken: undefined, + acceptEmergencyAccessInviteToken: acceptEmergencyAccessInviteToken, + acceptEmergencyAccessId: emergencyAccessId, + providerInviteToken: undefined, + providerUserId: undefined, + }), + ); + }); + + it("registers the user and returns a captcha bypass token when given a provider invite token", async () => { + keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]); + keyService.makeKeyPair.mockResolvedValue(userKeyPair); + accountApiService.registerFinish.mockResolvedValue(capchaBypassToken); + acceptOrgInviteService.getOrganizationInvite.mockResolvedValue(null); + + const result = await service.finishRegistration( + email, + passwordInputResult, + undefined, + undefined, + undefined, + undefined, + providerInviteToken, + providerUserId, + ); + + expect(result).toEqual(capchaBypassToken); + + expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey); + expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey); + expect(accountApiService.registerFinish).toHaveBeenCalledWith( + expect.objectContaining({ + email, + emailVerificationToken: undefined, + masterPasswordHash: passwordInputResult.masterKeyHash, + masterPasswordHint: passwordInputResult.hint, + userSymmetricKey: userKeyEncString.encryptedString, + userAsymmetricKeys: { + publicKey: userKeyPair[0], + encryptedPrivateKey: userKeyPair[1].encryptedString, + }, + kdf: passwordInputResult.kdfConfig.kdfType, + kdfIterations: passwordInputResult.kdfConfig.iterations, + kdfMemory: undefined, + kdfParallelism: undefined, + orgInviteToken: undefined, + organizationUserId: undefined, + orgSponsoredFreeFamilyPlanToken: undefined, + acceptEmergencyAccessInviteToken: undefined, + acceptEmergencyAccessId: undefined, + providerInviteToken: providerInviteToken, + providerUserId: providerUserId, }), ); }); 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 03afce6348..ea9278a24d 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 @@ -82,6 +82,8 @@ export class WebRegistrationFinishService orgSponsoredFreeFamilyPlanToken?: string, acceptEmergencyAccessInviteToken?: string, emergencyAccessId?: string, + providerInviteToken?: string, + providerUserId?: string, ): Promise { const registerRequest = await super.buildRegisterRequest( email, @@ -110,6 +112,11 @@ export class WebRegistrationFinishService registerRequest.acceptEmergencyAccessId = emergencyAccessId; } + if (providerInviteToken && providerUserId) { + registerRequest.providerInviteToken = providerInviteToken; + registerRequest.providerUserId = providerUserId; + } + return registerRequest; } } diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/accept-provider.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/accept-provider.component.html index 8e24bb7939..37d5001f77 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/accept-provider.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/accept-provider.component.html @@ -28,14 +28,8 @@ > {{ "logIn" | i18n }} - + diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/accept-provider.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/accept-provider.component.ts index 30effa880e..3b38cc4763 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/accept-provider.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/accept-provider.component.ts @@ -1,5 +1,6 @@ import { Component } from "@angular/core"; import { ActivatedRoute, Params, Router } from "@angular/router"; +import { firstValueFrom } from "rxjs"; import { RegisterRouteService } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -15,6 +16,9 @@ import { BaseAcceptComponent } from "@bitwarden/web-vault/app/common/base.accept }) export class AcceptProviderComponent extends BaseAcceptComponent { providerName: string; + providerId: string; + providerUserId: string; + providerInviteToken: string; failedMessage = "providerInviteAcceptFailed"; @@ -52,5 +56,31 @@ export class AcceptProviderComponent extends BaseAcceptComponent { async unauthedHandler(qParams: Params) { this.providerName = qParams.providerName; + this.providerId = qParams.providerId; + this.providerUserId = qParams.providerUserId; + this.providerInviteToken = qParams.token; + } + + async register() { + let queryParams: Params; + let registerRoute = await firstValueFrom(this.registerRoute$); + if (registerRoute === "/register") { + queryParams = { + email: this.email, + }; + } else if (registerRoute === "/signup") { + // We have to override the base component route as we don't need users to + // complete email verification if they are coming directly an emailed invite. + registerRoute = "/finish-signup"; + queryParams = { + email: this.email, + providerUserId: this.providerUserId, + providerInviteToken: this.providerInviteToken, + }; + } + + await this.router.navigate([registerRoute], { + queryParams: queryParams, + }); } } 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 bf0e75ed33..e034e23de4 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 @@ -113,8 +113,14 @@ describe("DefaultRegistrationFinishService", () => { kdfIterations: passwordInputResult.kdfConfig.iterations, kdfMemory: undefined, kdfParallelism: undefined, - orgInviteToken: undefined, // OrgInvite only handled in web - organizationUserId: undefined, // OrgInvite only handled in web + // Web only fields should be undefined + orgInviteToken: undefined, + organizationUserId: undefined, + orgSponsoredFreeFamilyPlanToken: undefined, + acceptEmergencyAccessInviteToken: undefined, + acceptEmergencyAccessId: undefined, + providerInviteToken: undefined, + providerUserId: undefined, }), ); }); 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 2443f77464..f2c4d4c98b 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 @@ -30,6 +30,8 @@ export class DefaultRegistrationFinishService implements RegistrationFinishServi orgSponsoredFreeFamilyPlanToken?: string, acceptEmergencyAccessInviteToken?: string, emergencyAccessId?: string, + providerInviteToken?: string, + providerUserId?: string, ): Promise { const [newUserKey, newEncUserKey] = await this.keyService.makeUserKey( passwordInputResult.masterKey, @@ -49,6 +51,8 @@ export class DefaultRegistrationFinishService implements RegistrationFinishServi orgSponsoredFreeFamilyPlanToken, acceptEmergencyAccessInviteToken, emergencyAccessId, + providerInviteToken, + providerUserId, ); const capchaBypassToken = await this.accountApiService.registerFinish(registerRequest); @@ -65,6 +69,8 @@ export class DefaultRegistrationFinishService implements RegistrationFinishServi orgSponsoredFreeFamilyPlanToken?: string, // web only acceptEmergencyAccessInviteToken?: string, // web only emergencyAccessId?: string, // web only + providerInviteToken?: string, // web only + providerUserId?: string, // web only ): Promise { const userAsymmetricKeysRequest = new KeysRequest( userAsymmetricKeys[0], diff --git a/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts b/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts index 3a6d26ef93..7cfa85ec3d 100644 --- a/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts +++ b/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts @@ -49,6 +49,10 @@ export class RegistrationFinishComponent implements OnInit, OnDestroy { acceptEmergencyAccessInviteToken: string; emergencyAccessId: string; + // This token is provided when the user is coming from an emailed invite to accept a provider invite + providerInviteToken: string; + providerUserId: string; + masterPasswordPolicyOptions: MasterPasswordPolicyOptions | null = null; constructor( @@ -104,6 +108,11 @@ export class RegistrationFinishComponent implements OnInit, OnDestroy { this.acceptEmergencyAccessInviteToken = qParams.acceptEmergencyAccessInviteToken; this.emergencyAccessId = qParams.emergencyAccessId; } + + if (qParams.providerInviteToken != null && qParams.providerUserId != null) { + this.providerInviteToken = qParams.providerInviteToken; + this.providerUserId = qParams.providerUserId; + } } private async initOrgInviteFlowIfPresent(): Promise { @@ -140,6 +149,8 @@ export class RegistrationFinishComponent implements OnInit, OnDestroy { this.orgSponsoredFreeFamilyPlanToken, this.acceptEmergencyAccessInviteToken, this.emergencyAccessId, + this.providerInviteToken, + this.providerUserId, ); } catch (e) { this.validationService.showError(e); diff --git a/libs/auth/src/angular/registration/registration-finish/registration-finish.service.ts b/libs/auth/src/angular/registration/registration-finish/registration-finish.service.ts index b7abd38108..3746e37b84 100644 --- a/libs/auth/src/angular/registration/registration-finish/registration-finish.service.ts +++ b/libs/auth/src/angular/registration/registration-finish/registration-finish.service.ts @@ -25,6 +25,8 @@ export abstract class RegistrationFinishService { * @param orgSponsoredFreeFamilyPlanToken The optional org sponsored free family plan token. * @param acceptEmergencyAccessInviteToken The optional accept emergency access invite token. * @param emergencyAccessId The optional emergency access id which is required to validate the emergency access invite token. + * @param providerInviteToken The optional provider invite token. + * @param providerUserId The optional provider user id which is required to validate the provider invite token. * @returns a promise which resolves to the captcha bypass token string upon a successful account creation. */ abstract finishRegistration( @@ -34,5 +36,7 @@ export abstract class RegistrationFinishService { orgSponsoredFreeFamilyPlanToken?: string, acceptEmergencyAccessInviteToken?: string, emergencyAccessId?: string, + providerInviteToken?: string, + providerUserId?: string, ): Promise; } 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 6a36bf8213..7ffac6bfe6 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 @@ -21,6 +21,8 @@ export class RegisterFinishRequest { 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,