mirror of
https://github.com/bitwarden/browser.git
synced 2024-12-21 16:18:28 +01:00
Auth/PM-12613 - Registration with Email Verification - Provider Invite Flow (#11635)
* PM-12613 - AcceptProviderComp - Add support for new registration with email verification flow. * PM-12613 - AcceptProviderComp - Reduce required params for finish registration to minimum * PM-12613 - RegistrationFinish - Add passthrough logic for provider invite token * PM-12613 - Update DefaultRegistrationFinishService finishRegistration tests to assert that all web only inputs are undefined on the outgoing request model * PM-12613 - DefaultRegistrationFinishService - finishRegistration - Add missed mapping of optional properties into buildRegisterRequest * PM-12613 - WebRegistrationFinishService - Add tests for additional token flows.
This commit is contained in:
parent
a0fe4f4ca6
commit
1fb1be56b3
@ -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<KeyService>;
|
||||
@ -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,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
@ -82,6 +82,8 @@ export class WebRegistrationFinishService
|
||||
orgSponsoredFreeFamilyPlanToken?: string,
|
||||
acceptEmergencyAccessInviteToken?: string,
|
||||
emergencyAccessId?: string,
|
||||
providerInviteToken?: string,
|
||||
providerUserId?: string,
|
||||
): Promise<RegisterFinishRequest> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -28,14 +28,8 @@
|
||||
>
|
||||
{{ "logIn" | i18n }}
|
||||
</a>
|
||||
<a
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
[routerLink]="registerRoute$ | async"
|
||||
[queryParams]="{ email: email }"
|
||||
[block]="true"
|
||||
>
|
||||
<button type="button" bitButton buttonType="primary" (click)="register()" [block]="true">
|
||||
{{ "createAccount" | i18n }}
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
@ -30,6 +30,8 @@ export class DefaultRegistrationFinishService implements RegistrationFinishServi
|
||||
orgSponsoredFreeFamilyPlanToken?: string,
|
||||
acceptEmergencyAccessInviteToken?: string,
|
||||
emergencyAccessId?: string,
|
||||
providerInviteToken?: string,
|
||||
providerUserId?: string,
|
||||
): Promise<string> {
|
||||
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<RegisterFinishRequest> {
|
||||
const userAsymmetricKeysRequest = new KeysRequest(
|
||||
userAsymmetricKeys[0],
|
||||
|
@ -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<boolean> {
|
||||
@ -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);
|
||||
|
@ -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<string>;
|
||||
}
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user