mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-21 11:35:34 +01:00
[PM-5951] Migrate org invite state (#9014)
* use deep linked url for org invite instead of separate state * remove organization invite state & fix tests * clear login redirect for SSO JIT users since they are accepted when setting MP * create accept org invite service and consolidate components in module * finish switch to accept org invite service * move logic to accept org service * the rest of the owl * clear org invite along with deep linked route * pr feedback * fix test and add error to catch null invite * pr feedback * clear stored invite if it doesn't match provided one
This commit is contained in:
parent
f79d1dac92
commit
e29025df28
@ -1,244 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { ActivatedRoute, Params, Router } from "@angular/router";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
|
||||
import {
|
||||
OrganizationUserAcceptInitRequest,
|
||||
OrganizationUserAcceptRequest,
|
||||
} from "@bitwarden/common/admin-console/abstractions/organization-user/requests";
|
||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { OrgKey } from "@bitwarden/common/types/key";
|
||||
|
||||
import { BaseAcceptComponent } from "../common/base.accept.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-accept-organization",
|
||||
templateUrl: "accept-organization.component.html",
|
||||
})
|
||||
export class AcceptOrganizationComponent extends BaseAcceptComponent {
|
||||
orgName: string;
|
||||
|
||||
protected requiredParameters: string[] = ["organizationId", "organizationUserId", "token"];
|
||||
|
||||
constructor(
|
||||
router: Router,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
i18nService: I18nService,
|
||||
route: ActivatedRoute,
|
||||
stateService: StateService,
|
||||
private cryptoService: CryptoService,
|
||||
private policyApiService: PolicyApiServiceAbstraction,
|
||||
private policyService: PolicyService,
|
||||
private logService: LogService,
|
||||
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||
private organizationUserService: OrganizationUserService,
|
||||
private messagingService: MessagingService,
|
||||
private apiService: ApiService,
|
||||
) {
|
||||
super(router, platformUtilsService, i18nService, route, stateService);
|
||||
}
|
||||
|
||||
async authedHandler(qParams: Params): Promise<void> {
|
||||
const initOrganization =
|
||||
qParams.initOrganization != null && qParams.initOrganization.toLocaleLowerCase() === "true";
|
||||
if (initOrganization) {
|
||||
this.actionPromise = this.acceptInitOrganizationFlow(qParams);
|
||||
} else {
|
||||
const needsReAuth = (await this.stateService.getOrganizationInvitation()) == null;
|
||||
if (needsReAuth) {
|
||||
// Accepting an org invite requires authentication from a logged out state
|
||||
this.messagingService.send("logout", { redirect: false });
|
||||
await this.prepareOrganizationInvitation(qParams);
|
||||
return;
|
||||
}
|
||||
|
||||
// User has already logged in and passed the Master Password policy check
|
||||
this.actionPromise = this.acceptFlow(qParams);
|
||||
}
|
||||
|
||||
await this.actionPromise;
|
||||
await this.apiService.refreshIdentityToken();
|
||||
await this.stateService.setOrganizationInvitation(null);
|
||||
this.platformUtilService.showToast(
|
||||
"success",
|
||||
this.i18nService.t("inviteAccepted"),
|
||||
initOrganization
|
||||
? this.i18nService.t("inviteInitAcceptedDesc")
|
||||
: this.i18nService.t("inviteAcceptedDesc"),
|
||||
{ timeout: 10000 },
|
||||
);
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["/vault"]);
|
||||
}
|
||||
|
||||
async unauthedHandler(qParams: Params): Promise<void> {
|
||||
await this.prepareOrganizationInvitation(qParams);
|
||||
|
||||
// In certain scenarios, we want to accelerate the user through the accept org invite process
|
||||
// For example, if the user has a BW account already, we want them to be taken to login instead of creation.
|
||||
await this.accelerateInviteAcceptIfPossible(qParams);
|
||||
}
|
||||
|
||||
private async acceptInitOrganizationFlow(qParams: Params): Promise<any> {
|
||||
return this.prepareAcceptInitRequest(qParams).then((request) =>
|
||||
this.organizationUserService.postOrganizationUserAcceptInit(
|
||||
qParams.organizationId,
|
||||
qParams.organizationUserId,
|
||||
request,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private async acceptFlow(qParams: Params): Promise<any> {
|
||||
return this.prepareAcceptRequest(qParams).then((request) =>
|
||||
this.organizationUserService.postOrganizationUserAccept(
|
||||
qParams.organizationId,
|
||||
qParams.organizationUserId,
|
||||
request,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private async prepareAcceptInitRequest(
|
||||
qParams: Params,
|
||||
): Promise<OrganizationUserAcceptInitRequest> {
|
||||
const request = new OrganizationUserAcceptInitRequest();
|
||||
request.token = qParams.token;
|
||||
|
||||
const [encryptedOrgKey, orgKey] = await this.cryptoService.makeOrgKey<OrgKey>();
|
||||
const [orgPublicKey, encryptedOrgPrivateKey] = await this.cryptoService.makeKeyPair(orgKey);
|
||||
const collection = await this.cryptoService.encrypt(
|
||||
this.i18nService.t("defaultCollection"),
|
||||
orgKey,
|
||||
);
|
||||
|
||||
request.key = encryptedOrgKey.encryptedString;
|
||||
request.keys = new OrganizationKeysRequest(
|
||||
orgPublicKey,
|
||||
encryptedOrgPrivateKey.encryptedString,
|
||||
);
|
||||
request.collectionName = collection.encryptedString;
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
private async prepareAcceptRequest(qParams: Params): Promise<OrganizationUserAcceptRequest> {
|
||||
const request = new OrganizationUserAcceptRequest();
|
||||
request.token = qParams.token;
|
||||
|
||||
if (await this.performResetPasswordAutoEnroll(qParams)) {
|
||||
const response = await this.organizationApiService.getKeys(qParams.organizationId);
|
||||
|
||||
if (response == null) {
|
||||
throw new Error(this.i18nService.t("resetPasswordOrgKeysError"));
|
||||
}
|
||||
|
||||
const publicKey = Utils.fromB64ToArray(response.publicKey);
|
||||
|
||||
// RSA Encrypt user's encKey.key with organization public key
|
||||
const userKey = await this.cryptoService.getUserKey();
|
||||
const encryptedKey = await this.cryptoService.rsaEncrypt(userKey.key, publicKey);
|
||||
|
||||
// Add reset password key to accept request
|
||||
request.resetPasswordKey = encryptedKey.encryptedString;
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
private async performResetPasswordAutoEnroll(qParams: Params): Promise<boolean> {
|
||||
let policyList: Policy[] = null;
|
||||
try {
|
||||
const policies = await this.policyApiService.getPoliciesByToken(
|
||||
qParams.organizationId,
|
||||
qParams.token,
|
||||
qParams.email,
|
||||
qParams.organizationUserId,
|
||||
);
|
||||
policyList = Policy.fromListResponse(policies);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
|
||||
if (policyList != null) {
|
||||
const result = this.policyService.getResetPasswordPolicyOptions(
|
||||
policyList,
|
||||
qParams.organizationId,
|
||||
);
|
||||
// Return true if policy enabled and auto-enroll enabled
|
||||
return result[1] && result[0].autoEnrollEnabled;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async prepareOrganizationInvitation(qParams: Params): Promise<void> {
|
||||
this.orgName = qParams.organizationName;
|
||||
if (this.orgName != null) {
|
||||
// Fix URL encoding of space issue with Angular
|
||||
this.orgName = this.orgName.replace(/\+/g, " ");
|
||||
}
|
||||
await this.stateService.setOrganizationInvitation(qParams);
|
||||
}
|
||||
|
||||
private async accelerateInviteAcceptIfPossible(qParams: Params): Promise<void> {
|
||||
// Extract the query params we need to make routing acceleration decisions
|
||||
const orgSsoIdentifier = qParams.orgSsoIdentifier;
|
||||
const orgUserHasExistingUser = this.stringToNullOrBool(qParams.orgUserHasExistingUser);
|
||||
|
||||
// if orgUserHasExistingUser is null, short circuit for backwards compatibility w/ older servers
|
||||
if (orgUserHasExistingUser == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if user exists, send user to login
|
||||
if (orgUserHasExistingUser) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["/login"], {
|
||||
queryParams: { email: qParams.email },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// no user exists; so either sign in via SSO and JIT provision one or simply register.
|
||||
|
||||
if (orgSsoIdentifier) {
|
||||
// We only send sso org identifier if the org has SSO enabled and the SSO policy required.
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["/sso"], {
|
||||
queryParams: { email: qParams.email, identifier: orgSsoIdentifier },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// if SSO is disabled OR if sso is enabled but the SSO login required policy is not enabled
|
||||
// then send user to create account
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["/register"], {
|
||||
queryParams: { email: qParams.email, fromOrgInvite: true },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
private stringToNullOrBool(s: string | undefined): boolean | null {
|
||||
if (s === undefined) {
|
||||
return null;
|
||||
}
|
||||
return s.toLowerCase() === "true";
|
||||
}
|
||||
}
|
@ -1,9 +1,10 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { AcceptOrganizationInviteModule } from "./organization-invite/accept-organization.module";
|
||||
import { AuthSettingsModule } from "./settings/settings.module";
|
||||
|
||||
@NgModule({
|
||||
imports: [AuthSettingsModule],
|
||||
imports: [AuthSettingsModule, AcceptOrganizationInviteModule],
|
||||
declarations: [],
|
||||
providers: [],
|
||||
exports: [AuthSettingsModule],
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { ActivatedRoute, Params, Router } from "@angular/router";
|
||||
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
|
||||
import { BaseAcceptComponent } from "../../../common/base.accept.component";
|
||||
import { SharedModule } from "../../../shared";
|
||||
@ -27,10 +27,10 @@ export class AcceptEmergencyComponent extends BaseAcceptComponent {
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
i18nService: I18nService,
|
||||
route: ActivatedRoute,
|
||||
stateService: StateService,
|
||||
authService: AuthService,
|
||||
private emergencyAccessService: EmergencyAccessService,
|
||||
) {
|
||||
super(router, platformUtilsService, i18nService, route, stateService);
|
||||
super(router, platformUtilsService, i18nService, route, authService);
|
||||
}
|
||||
|
||||
async authedHandler(qParams: Params): Promise<void> {
|
||||
|
@ -1,14 +1,27 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { Component, inject } from "@angular/core";
|
||||
|
||||
import { BaseLoginDecryptionOptionsComponent } from "@bitwarden/angular/auth/components/base-login-decryption-options.component";
|
||||
|
||||
import { RouterService } from "../../../core";
|
||||
import { AcceptOrganizationInviteService } from "../../organization-invite/accept-organization.service";
|
||||
@Component({
|
||||
selector: "web-login-decryption-options",
|
||||
templateUrl: "login-decryption-options.component.html",
|
||||
})
|
||||
export class LoginDecryptionOptionsComponent extends BaseLoginDecryptionOptionsComponent {
|
||||
protected routerService = inject(RouterService);
|
||||
protected acceptOrganizationInviteService = inject(AcceptOrganizationInviteService);
|
||||
|
||||
override async createUser(): Promise<void> {
|
||||
try {
|
||||
await super.createUser();
|
||||
|
||||
// Invites from TDE orgs go through here, but the invite is
|
||||
// accepted while being enrolled in admin recovery. So we need to clear
|
||||
// the redirect and stored org invite.
|
||||
await this.routerService.getAndClearLoginRedirectUrl();
|
||||
await this.acceptOrganizationInviteService.clearOrganizationInvitation();
|
||||
|
||||
await this.router.navigate(["/vault"]);
|
||||
} catch (error) {
|
||||
this.validationService.showError(error);
|
||||
|
@ -15,12 +15,10 @@ import { InternalPolicyService } from "@bitwarden/common/admin-console/abstracti
|
||||
import { PolicyData } from "@bitwarden/common/admin-console/models/data/policy.data";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response";
|
||||
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction";
|
||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
@ -32,6 +30,8 @@ import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/pass
|
||||
|
||||
import { flagEnabled } from "../../../utils/flags";
|
||||
import { RouterService, StateService } from "../../core";
|
||||
import { AcceptOrganizationInviteService } from "../organization-invite/accept-organization.service";
|
||||
import { OrganizationInvite } from "../organization-invite/organization-invite";
|
||||
|
||||
@Component({
|
||||
selector: "app-login",
|
||||
@ -41,10 +41,11 @@ import { RouterService, StateService } from "../../core";
|
||||
export class LoginComponent extends BaseLoginComponent implements OnInit {
|
||||
showResetPasswordAutoEnrollWarning = false;
|
||||
enforcedPasswordPolicyOptions: MasterPasswordPolicyOptions;
|
||||
policies: ListResponse<PolicyResponse>;
|
||||
policies: Policy[];
|
||||
showPasswordless = false;
|
||||
|
||||
constructor(
|
||||
private acceptOrganizationInviteService: AcceptOrganizationInviteService,
|
||||
devicesApiService: DevicesApiServiceAbstraction,
|
||||
appIdService: AppIdService,
|
||||
loginStrategyService: LoginStrategyServiceAbstraction,
|
||||
@ -112,37 +113,10 @@ export class LoginComponent extends BaseLoginComponent implements OnInit {
|
||||
await super.ngOnInit();
|
||||
});
|
||||
|
||||
const invite = await this.stateService.getOrganizationInvitation();
|
||||
if (invite != null) {
|
||||
let policyList: Policy[] = null;
|
||||
try {
|
||||
this.policies = await this.policyApiService.getPoliciesByToken(
|
||||
invite.organizationId,
|
||||
invite.token,
|
||||
invite.email,
|
||||
invite.organizationUserId,
|
||||
);
|
||||
policyList = Policy.fromListResponse(this.policies);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
|
||||
if (policyList != null) {
|
||||
const resetPasswordPolicy = this.policyService.getResetPasswordPolicyOptions(
|
||||
policyList,
|
||||
invite.organizationId,
|
||||
);
|
||||
// Set to true if policy enabled and auto-enroll enabled
|
||||
this.showResetPasswordAutoEnrollWarning =
|
||||
resetPasswordPolicy[1] && resetPasswordPolicy[0].autoEnrollEnabled;
|
||||
|
||||
this.policyService
|
||||
.masterPasswordPolicyOptions$(policyList)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((enforcedPasswordPolicyOptions) => {
|
||||
this.enforcedPasswordPolicyOptions = enforcedPasswordPolicyOptions;
|
||||
});
|
||||
}
|
||||
// If there's an existing org invite, use it to get the password policies
|
||||
const orgInvite = await this.acceptOrganizationInviteService.getOrganizationInvite();
|
||||
if (orgInvite != null) {
|
||||
await this.initPasswordPolicies(orgInvite);
|
||||
}
|
||||
}
|
||||
|
||||
@ -166,50 +140,69 @@ export class LoginComponent extends BaseLoginComponent implements OnInit {
|
||||
)
|
||||
) {
|
||||
const policiesData: { [id: string]: PolicyData } = {};
|
||||
this.policies.data.map((p) => (policiesData[p.id] = new PolicyData(p)));
|
||||
this.policies.map((p) => (policiesData[p.id] = PolicyData.fromPolicy(p)));
|
||||
await this.policyService.replace(policiesData);
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["update-password"]);
|
||||
await this.router.navigate(["update-password"]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.loginEmailService.clearValues();
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate([this.successRoute]);
|
||||
await this.router.navigate([this.successRoute]);
|
||||
}
|
||||
|
||||
goToHint() {
|
||||
async goToHint() {
|
||||
this.setLoginEmailValues();
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigateByUrl("/hint");
|
||||
await this.router.navigateByUrl("/hint");
|
||||
}
|
||||
|
||||
goToRegister() {
|
||||
async goToRegister() {
|
||||
const email = this.formGroup.value.email;
|
||||
|
||||
if (email) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["/register"], { queryParams: { email: email } });
|
||||
await this.router.navigate(["/register"], { queryParams: { email: email } });
|
||||
return;
|
||||
}
|
||||
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["/register"]);
|
||||
await this.router.navigate(["/register"]);
|
||||
}
|
||||
|
||||
protected override handleMigrateEncryptionKey(result: AuthResult): boolean {
|
||||
protected override async handleMigrateEncryptionKey(result: AuthResult): Promise<boolean> {
|
||||
if (!result.requiresEncryptionKeyMigration) {
|
||||
return false;
|
||||
}
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["migrate-legacy-encryption"]);
|
||||
await this.router.navigate(["migrate-legacy-encryption"]);
|
||||
return true;
|
||||
}
|
||||
|
||||
private async initPasswordPolicies(invite: OrganizationInvite): Promise<void> {
|
||||
try {
|
||||
this.policies = await this.policyApiService.getPoliciesByToken(
|
||||
invite.organizationId,
|
||||
invite.token,
|
||||
invite.email,
|
||||
invite.organizationUserId,
|
||||
);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
|
||||
if (this.policies == null) {
|
||||
return;
|
||||
}
|
||||
const resetPasswordPolicy = this.policyService.getResetPasswordPolicyOptions(
|
||||
this.policies,
|
||||
invite.organizationId,
|
||||
);
|
||||
// Set to true if policy enabled and auto-enroll enabled
|
||||
this.showResetPasswordAutoEnrollWarning =
|
||||
resetPasswordPolicy[1] && resetPasswordPolicy[0].autoEnrollEnabled;
|
||||
|
||||
this.policyService
|
||||
.masterPasswordPolicyOptions$(this.policies)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((enforcedPasswordPolicyOptions) => {
|
||||
this.enforcedPasswordPolicyOptions = enforcedPasswordPolicyOptions;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,7 @@
|
||||
<div class="card d-block">
|
||||
<div class="card-body">
|
||||
<p class="text-center">
|
||||
{{ orgName }}
|
||||
{{ orgName$ | async }}
|
||||
<strong class="d-block mt-2">{{ email }}</strong>
|
||||
</p>
|
||||
<p>{{ "joinOrganizationDesc" | i18n }}</p>
|
@ -0,0 +1,94 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { ActivatedRoute, Params, Router } from "@angular/router";
|
||||
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
||||
import { BaseAcceptComponent } from "../../common/base.accept.component";
|
||||
|
||||
import { AcceptOrganizationInviteService } from "./accept-organization.service";
|
||||
import { OrganizationInvite } from "./organization-invite";
|
||||
|
||||
@Component({
|
||||
templateUrl: "accept-organization.component.html",
|
||||
})
|
||||
export class AcceptOrganizationComponent extends BaseAcceptComponent {
|
||||
orgName$ = this.acceptOrganizationInviteService.orgName$;
|
||||
protected requiredParameters: string[] = ["organizationId", "organizationUserId", "token"];
|
||||
|
||||
constructor(
|
||||
router: Router,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
i18nService: I18nService,
|
||||
route: ActivatedRoute,
|
||||
authService: AuthService,
|
||||
private acceptOrganizationInviteService: AcceptOrganizationInviteService,
|
||||
) {
|
||||
super(router, platformUtilsService, i18nService, route, authService);
|
||||
}
|
||||
|
||||
async authedHandler(qParams: Params): Promise<void> {
|
||||
const invite = OrganizationInvite.fromParams(qParams);
|
||||
const success = await this.acceptOrganizationInviteService.validateAndAcceptInvite(invite);
|
||||
|
||||
if (!success) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.platformUtilService.showToast(
|
||||
"success",
|
||||
this.i18nService.t("inviteAccepted"),
|
||||
invite.initOrganization
|
||||
? this.i18nService.t("inviteInitAcceptedDesc")
|
||||
: this.i18nService.t("inviteAcceptedDesc"),
|
||||
{ timeout: 10000 },
|
||||
);
|
||||
|
||||
await this.router.navigate(["/vault"]);
|
||||
}
|
||||
|
||||
async unauthedHandler(qParams: Params): Promise<void> {
|
||||
const invite = OrganizationInvite.fromParams(qParams);
|
||||
await this.acceptOrganizationInviteService.setOrganizationInvitation(invite);
|
||||
await this.accelerateInviteAcceptIfPossible(invite);
|
||||
}
|
||||
|
||||
/**
|
||||
* In certain scenarios, we want to accelerate the user through the accept org invite process
|
||||
* For example, if the user has a BW account already, we want them to be taken to login instead of creation.
|
||||
*/
|
||||
private async accelerateInviteAcceptIfPossible(invite: OrganizationInvite): Promise<void> {
|
||||
// if orgUserHasExistingUser is null, we can't determine the user's status
|
||||
// so we don't want to accelerate the process
|
||||
if (invite.orgUserHasExistingUser == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if user exists, send user to login
|
||||
if (invite.orgUserHasExistingUser) {
|
||||
await this.router.navigate(["/login"], {
|
||||
queryParams: { email: invite.email },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (invite.orgSsoIdentifier) {
|
||||
// We only send sso org identifier if the org has SSO enabled and the SSO policy required.
|
||||
// Will JIT provision the user.
|
||||
// Note: If the organization has Admin Recovery enabled, the user will be accepted into the org
|
||||
// upon enrollment. The user should not be returned here.
|
||||
await this.router.navigate(["/sso"], {
|
||||
queryParams: { email: invite.email, identifier: invite.orgSsoIdentifier },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// if SSO is disabled OR if sso is enabled but the SSO login required policy is not enabled
|
||||
// then send user to create account
|
||||
await this.router.navigate(["/register"], {
|
||||
queryParams: { email: invite.email, fromOrgInvite: true },
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { SharedModule } from "../../shared";
|
||||
|
||||
import { AcceptOrganizationComponent } from "./accept-organization.component";
|
||||
import { AcceptOrganizationInviteService } from "./accept-organization.service";
|
||||
|
||||
@NgModule({
|
||||
declarations: [AcceptOrganizationComponent],
|
||||
imports: [SharedModule],
|
||||
providers: [AcceptOrganizationInviteService],
|
||||
})
|
||||
export class AcceptOrganizationInviteModule {}
|
@ -0,0 +1,185 @@
|
||||
import { FakeGlobalStateProvider } from "@bitwarden/common/../spec/fake-state-provider";
|
||||
import { MockProxy, mock } from "jest-mock-extended";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
|
||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { ResetPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/reset-password-policy-options";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { FakeGlobalState } from "@bitwarden/common/spec/fake-state";
|
||||
import { OrgKey } from "@bitwarden/common/types/key";
|
||||
|
||||
import { I18nService } from "../../core/i18n.service";
|
||||
|
||||
import {
|
||||
AcceptOrganizationInviteService,
|
||||
ORGANIZATION_INVITE,
|
||||
} from "./accept-organization.service";
|
||||
import { OrganizationInvite } from "./organization-invite";
|
||||
|
||||
describe("AcceptOrganizationInviteService", () => {
|
||||
let sut: AcceptOrganizationInviteService;
|
||||
let apiService: MockProxy<ApiService>;
|
||||
let authService: MockProxy<AuthService>;
|
||||
let cryptoService: MockProxy<CryptoService>;
|
||||
let encryptService: MockProxy<EncryptService>;
|
||||
let policyApiService: MockProxy<PolicyApiServiceAbstraction>;
|
||||
let policyService: MockProxy<PolicyService>;
|
||||
let logService: MockProxy<LogService>;
|
||||
let organizationApiService: MockProxy<OrganizationApiServiceAbstraction>;
|
||||
let organizationUserService: MockProxy<OrganizationUserService>;
|
||||
let i18nService: MockProxy<I18nService>;
|
||||
let globalStateProvider: FakeGlobalStateProvider;
|
||||
let globalState: FakeGlobalState<OrganizationInvite>;
|
||||
|
||||
beforeEach(() => {
|
||||
apiService = mock();
|
||||
authService = mock();
|
||||
cryptoService = mock();
|
||||
encryptService = mock();
|
||||
policyApiService = mock();
|
||||
policyService = mock();
|
||||
logService = mock();
|
||||
organizationApiService = mock();
|
||||
organizationUserService = mock();
|
||||
i18nService = mock();
|
||||
globalStateProvider = new FakeGlobalStateProvider();
|
||||
globalState = globalStateProvider.getFake(ORGANIZATION_INVITE);
|
||||
|
||||
sut = new AcceptOrganizationInviteService(
|
||||
apiService,
|
||||
authService,
|
||||
cryptoService,
|
||||
encryptService,
|
||||
policyApiService,
|
||||
policyService,
|
||||
logService,
|
||||
organizationApiService,
|
||||
organizationUserService,
|
||||
i18nService,
|
||||
globalStateProvider,
|
||||
);
|
||||
});
|
||||
|
||||
describe("validateAndAcceptInvite", () => {
|
||||
it("initializes an organization when given an invite where initOrganization is true", async () => {
|
||||
cryptoService.makeOrgKey.mockResolvedValue([
|
||||
{ encryptedString: "string" } as EncString,
|
||||
"orgPrivateKey" as unknown as OrgKey,
|
||||
]);
|
||||
cryptoService.makeKeyPair.mockResolvedValue([
|
||||
"orgPublicKey",
|
||||
{ encryptedString: "string" } as EncString,
|
||||
]);
|
||||
encryptService.encrypt.mockResolvedValue({ encryptedString: "string" } as EncString);
|
||||
const invite = createOrgInvite({ initOrganization: true });
|
||||
|
||||
const result = await sut.validateAndAcceptInvite(invite);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(organizationUserService.postOrganizationUserAcceptInit).toHaveBeenCalled();
|
||||
expect(apiService.refreshIdentityToken).toHaveBeenCalled();
|
||||
expect(globalState.nextMock).toHaveBeenCalledWith(null);
|
||||
expect(organizationUserService.postOrganizationUserAccept).not.toHaveBeenCalled();
|
||||
expect(authService.logOut).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("logs out the user and stores the invite when a master password policy check is required", async () => {
|
||||
const invite = createOrgInvite();
|
||||
policyApiService.getPoliciesByToken.mockResolvedValue([
|
||||
{
|
||||
type: PolicyType.MasterPassword,
|
||||
enabled: true,
|
||||
} as Policy,
|
||||
]);
|
||||
|
||||
const result = await sut.validateAndAcceptInvite(invite);
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(authService.logOut).toHaveBeenCalled();
|
||||
expect(globalState.nextMock).toHaveBeenCalledWith(invite);
|
||||
});
|
||||
|
||||
it("clears the stored invite when a master password policy check is required but the stored invite doesn't match the provided one", async () => {
|
||||
const storedInvite = createOrgInvite({ email: "wrongemail@example.com" });
|
||||
const providedInvite = createOrgInvite();
|
||||
await globalState.update(() => storedInvite);
|
||||
policyApiService.getPoliciesByToken.mockResolvedValue([
|
||||
{
|
||||
type: PolicyType.MasterPassword,
|
||||
enabled: true,
|
||||
} as Policy,
|
||||
]);
|
||||
|
||||
const result = await sut.validateAndAcceptInvite(providedInvite);
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(authService.logOut).toHaveBeenCalled();
|
||||
expect(globalState.nextMock).toHaveBeenCalledWith(providedInvite);
|
||||
});
|
||||
|
||||
it("accepts the invitation request when the organization doesn't have a master password policy", async () => {
|
||||
const invite = createOrgInvite();
|
||||
policyApiService.getPoliciesByToken.mockResolvedValue([]);
|
||||
|
||||
const result = await sut.validateAndAcceptInvite(invite);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(organizationUserService.postOrganizationUserAccept).toHaveBeenCalled();
|
||||
expect(apiService.refreshIdentityToken).toHaveBeenCalled();
|
||||
expect(globalState.nextMock).toHaveBeenCalledWith(null);
|
||||
expect(organizationUserService.postOrganizationUserAcceptInit).not.toHaveBeenCalled();
|
||||
expect(authService.logOut).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("accepts the invitation request when the org has a master password policy, but the user has already passed it", async () => {
|
||||
const invite = createOrgInvite();
|
||||
policyApiService.getPoliciesByToken.mockResolvedValue([
|
||||
{
|
||||
type: PolicyType.MasterPassword,
|
||||
enabled: true,
|
||||
} as Policy,
|
||||
]);
|
||||
// an existing invite means the user has already passed the master password policy
|
||||
await globalState.update(() => invite);
|
||||
|
||||
policyService.getResetPasswordPolicyOptions.mockReturnValue([
|
||||
{
|
||||
autoEnrollEnabled: false,
|
||||
} as ResetPasswordPolicyOptions,
|
||||
false,
|
||||
]);
|
||||
|
||||
const result = await sut.validateAndAcceptInvite(invite);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(organizationUserService.postOrganizationUserAccept).toHaveBeenCalled();
|
||||
expect(organizationUserService.postOrganizationUserAcceptInit).not.toHaveBeenCalled();
|
||||
expect(authService.logOut).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createOrgInvite(custom: Partial<OrganizationInvite> = {}): OrganizationInvite {
|
||||
return Object.assign(
|
||||
{
|
||||
email: "user@example.com",
|
||||
initOrganization: false,
|
||||
orgSsoIdentifier: null,
|
||||
orgUserHasExistingUser: false,
|
||||
organizationId: "organizationId",
|
||||
organizationName: "organizationName",
|
||||
organizationUserId: "organizationUserId",
|
||||
token: "token",
|
||||
},
|
||||
custom,
|
||||
);
|
||||
}
|
@ -0,0 +1,248 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { BehaviorSubject, firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
|
||||
import {
|
||||
OrganizationUserAcceptRequest,
|
||||
OrganizationUserAcceptInitRequest,
|
||||
} from "@bitwarden/common/admin-console/abstractions/organization-user/requests";
|
||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import {
|
||||
GlobalState,
|
||||
GlobalStateProvider,
|
||||
KeyDefinition,
|
||||
ORGANIZATION_INVITE_DISK,
|
||||
} from "@bitwarden/common/platform/state";
|
||||
import { OrgKey } from "@bitwarden/common/types/key";
|
||||
|
||||
import { OrganizationInvite } from "./organization-invite";
|
||||
|
||||
// We're storing the organization invite for 2 reasons:
|
||||
// 1. If the org requires a MP policy check, we need to keep track that the user has already been redirected when they return.
|
||||
// 2. The MP policy check happens on login/register flows, we need to store the token to retrieve the policies then.
|
||||
export const ORGANIZATION_INVITE = new KeyDefinition<OrganizationInvite>(
|
||||
ORGANIZATION_INVITE_DISK,
|
||||
"organizationInvite",
|
||||
{
|
||||
deserializer: (invite) => OrganizationInvite.fromJSON(invite),
|
||||
},
|
||||
);
|
||||
|
||||
@Injectable()
|
||||
export class AcceptOrganizationInviteService {
|
||||
private organizationInvitationState: GlobalState<OrganizationInvite>;
|
||||
private orgNameSubject: BehaviorSubject<string> = new BehaviorSubject<string>(null);
|
||||
private policyCache: Policy[];
|
||||
|
||||
// Fix URL encoding of space issue with Angular
|
||||
orgName$ = this.orgNameSubject.pipe(map((orgName) => orgName.replace(/\+/g, " ")));
|
||||
|
||||
constructor(
|
||||
private readonly apiService: ApiService,
|
||||
private readonly authService: AuthService,
|
||||
private readonly cryptoService: CryptoService,
|
||||
private readonly encryptService: EncryptService,
|
||||
private readonly policyApiService: PolicyApiServiceAbstraction,
|
||||
private readonly policyService: PolicyService,
|
||||
private readonly logService: LogService,
|
||||
private readonly organizationApiService: OrganizationApiServiceAbstraction,
|
||||
private readonly organizationUserService: OrganizationUserService,
|
||||
private readonly i18nService: I18nService,
|
||||
private readonly globalStateProvider: GlobalStateProvider,
|
||||
) {
|
||||
this.organizationInvitationState = this.globalStateProvider.get(ORGANIZATION_INVITE);
|
||||
}
|
||||
|
||||
/** Returns the currently stored organization invite */
|
||||
async getOrganizationInvite(): Promise<OrganizationInvite> {
|
||||
return await firstValueFrom(this.organizationInvitationState.state$);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores a new organization invite
|
||||
* @param invite an organization invite
|
||||
* @throws if the invite is nullish
|
||||
*/
|
||||
async setOrganizationInvitation(invite: OrganizationInvite): Promise<void> {
|
||||
if (invite == null) {
|
||||
throw new Error("Invite cannot be null. Use clearOrganizationInvitation instead.");
|
||||
}
|
||||
await this.organizationInvitationState.update(() => invite);
|
||||
}
|
||||
|
||||
/** Clears the currently stored organization invite */
|
||||
async clearOrganizationInvitation(): Promise<void> {
|
||||
await this.organizationInvitationState.update(() => null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates and accepts the organization invitation if possible.
|
||||
* Note: Users might need to pass a MP policy check before accepting an invite to an existing organization. If the user
|
||||
* has not passed this check, they will be logged out and the invite will be stored for later use.
|
||||
* @param invite an organization invite
|
||||
* @returns a promise that resolves a boolean indicating if the invite was accepted.
|
||||
*/
|
||||
async validateAndAcceptInvite(invite: OrganizationInvite): Promise<boolean> {
|
||||
if (invite == null) {
|
||||
throw new Error("Invite cannot be null.");
|
||||
}
|
||||
|
||||
// Creation of a new org
|
||||
if (invite.initOrganization) {
|
||||
await this.acceptAndInitOrganization(invite);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Accepting an org invite from existing org
|
||||
if (await this.masterPasswordPolicyCheckRequired(invite)) {
|
||||
await this.setOrganizationInvitation(invite);
|
||||
this.authService.logOut(() => {
|
||||
/* Do nothing */
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
// We know the user has already logged in and passed a MP policy check
|
||||
await this.accept(invite);
|
||||
return true;
|
||||
}
|
||||
|
||||
private async acceptAndInitOrganization(invite: OrganizationInvite): Promise<void> {
|
||||
await this.prepareAcceptAndInitRequest(invite).then((request) =>
|
||||
this.organizationUserService.postOrganizationUserAcceptInit(
|
||||
invite.organizationId,
|
||||
invite.organizationUserId,
|
||||
request,
|
||||
),
|
||||
);
|
||||
await this.apiService.refreshIdentityToken();
|
||||
await this.clearOrganizationInvitation();
|
||||
}
|
||||
|
||||
private async prepareAcceptAndInitRequest(
|
||||
invite: OrganizationInvite,
|
||||
): Promise<OrganizationUserAcceptInitRequest> {
|
||||
const request = new OrganizationUserAcceptInitRequest();
|
||||
request.token = invite.token;
|
||||
|
||||
const [encryptedOrgKey, orgKey] = await this.cryptoService.makeOrgKey<OrgKey>();
|
||||
const [orgPublicKey, encryptedOrgPrivateKey] = await this.cryptoService.makeKeyPair(orgKey);
|
||||
const collection = await this.encryptService.encrypt(
|
||||
this.i18nService.t("defaultCollection"),
|
||||
orgKey,
|
||||
);
|
||||
|
||||
request.key = encryptedOrgKey.encryptedString;
|
||||
request.keys = new OrganizationKeysRequest(
|
||||
orgPublicKey,
|
||||
encryptedOrgPrivateKey.encryptedString,
|
||||
);
|
||||
request.collectionName = collection.encryptedString;
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
private async accept(invite: OrganizationInvite): Promise<void> {
|
||||
await this.prepareAcceptRequest(invite).then((request) =>
|
||||
this.organizationUserService.postOrganizationUserAccept(
|
||||
invite.organizationId,
|
||||
invite.organizationUserId,
|
||||
request,
|
||||
),
|
||||
);
|
||||
|
||||
await this.apiService.refreshIdentityToken();
|
||||
await this.clearOrganizationInvitation();
|
||||
}
|
||||
|
||||
private async prepareAcceptRequest(
|
||||
invite: OrganizationInvite,
|
||||
): Promise<OrganizationUserAcceptRequest> {
|
||||
const request = new OrganizationUserAcceptRequest();
|
||||
request.token = invite.token;
|
||||
|
||||
if (await this.resetPasswordEnrollRequired(invite)) {
|
||||
const response = await this.organizationApiService.getKeys(invite.organizationId);
|
||||
|
||||
if (response == null) {
|
||||
throw new Error(this.i18nService.t("resetPasswordOrgKeysError"));
|
||||
}
|
||||
|
||||
const publicKey = Utils.fromB64ToArray(response.publicKey);
|
||||
|
||||
// RSA Encrypt user's encKey.key with organization public key
|
||||
const userKey = await this.cryptoService.getUserKey();
|
||||
const encryptedKey = await this.cryptoService.rsaEncrypt(userKey.key, publicKey);
|
||||
|
||||
// Add reset password key to accept request
|
||||
request.resetPasswordKey = encryptedKey.encryptedString;
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
private async resetPasswordEnrollRequired(invite: OrganizationInvite): Promise<boolean> {
|
||||
const policies = await this.getPolicies(invite);
|
||||
|
||||
if (policies == null || policies.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const result = this.policyService.getResetPasswordPolicyOptions(
|
||||
policies,
|
||||
invite.organizationId,
|
||||
);
|
||||
// Return true if policy enabled and auto-enroll enabled
|
||||
return result[1] && result[0].autoEnrollEnabled;
|
||||
}
|
||||
|
||||
private async masterPasswordPolicyCheckRequired(invite: OrganizationInvite): Promise<boolean> {
|
||||
const policies = await this.getPolicies(invite);
|
||||
|
||||
if (policies == null || policies.length === 0) {
|
||||
return false;
|
||||
}
|
||||
const hasMasterPasswordPolicy = policies.some(
|
||||
(p) => p.type === PolicyType.MasterPassword && p.enabled,
|
||||
);
|
||||
|
||||
let storedInvite = await this.getOrganizationInvite();
|
||||
if (storedInvite?.email !== invite.email) {
|
||||
// clear stored invites if the email doesn't match
|
||||
await this.clearOrganizationInvitation();
|
||||
storedInvite = null;
|
||||
}
|
||||
// if we don't have an org invite stored, we know the user hasn't been redirected yet to check the MP policy
|
||||
const hasNotCheckedMasterPasswordYet = storedInvite == null;
|
||||
return hasMasterPasswordPolicy && hasNotCheckedMasterPasswordYet;
|
||||
}
|
||||
|
||||
private async getPolicies(invite: OrganizationInvite): Promise<Policy[] | null> {
|
||||
// if policies are not cached, fetch them
|
||||
if (this.policyCache == null) {
|
||||
try {
|
||||
this.policyCache = await this.policyApiService.getPoliciesByToken(
|
||||
invite.organizationId,
|
||||
invite.token,
|
||||
invite.email,
|
||||
invite.organizationUserId,
|
||||
);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
return this.policyCache;
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
import { Params } from "@angular/router";
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
export class OrganizationInvite {
|
||||
email: string;
|
||||
initOrganization: boolean;
|
||||
orgSsoIdentifier: string;
|
||||
orgUserHasExistingUser: boolean;
|
||||
organizationId: string;
|
||||
organizationName: string;
|
||||
organizationUserId: string;
|
||||
token: string;
|
||||
|
||||
static fromJSON(json: Jsonify<OrganizationInvite>) {
|
||||
return Object.assign(new OrganizationInvite(), json);
|
||||
}
|
||||
|
||||
static fromParams(params: Params): OrganizationInvite {
|
||||
return Object.assign(new OrganizationInvite(), {
|
||||
email: params.email,
|
||||
initOrganization: params.initOrganization?.toLocaleLowerCase() === "true",
|
||||
orgSsoIdentifier: params.orgSsoIdentifier,
|
||||
orgUserHasExistingUser: params.orgUserHasExistingUser?.toLocaleLowerCase() === "true",
|
||||
organizationId: params.organizationId,
|
||||
organizationName: params.organizationName,
|
||||
organizationUserId: params.organizationUserId,
|
||||
token: params.token,
|
||||
});
|
||||
}
|
||||
}
|
@ -10,6 +10,7 @@ import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { ReferenceEventRequest } from "@bitwarden/common/models/request/reference-event.request";
|
||||
import { RegisterRequest } from "@bitwarden/common/models/request/register.request";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@ -19,6 +20,8 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { AcceptOrganizationInviteService } from "../organization-invite/accept-organization.service";
|
||||
|
||||
@Component({
|
||||
selector: "app-register-form",
|
||||
templateUrl: "./register-form.component.html",
|
||||
@ -48,6 +51,7 @@ export class RegisterFormComponent extends BaseRegisterComponent {
|
||||
logService: LogService,
|
||||
auditService: AuditService,
|
||||
dialogService: DialogService,
|
||||
acceptOrgInviteService: AcceptOrganizationInviteService,
|
||||
) {
|
||||
super(
|
||||
formValidationErrorService,
|
||||
@ -65,6 +69,16 @@ export class RegisterFormComponent extends BaseRegisterComponent {
|
||||
auditService,
|
||||
dialogService,
|
||||
);
|
||||
super.modifyRegisterRequest = async (request: RegisterRequest) => {
|
||||
// Org invites are deep linked. Non-existent accounts are redirected to the register page.
|
||||
// Org user id and token are included here only for validation and two factor purposes.
|
||||
const orgInvite = await acceptOrgInviteService.getOrganizationInvite();
|
||||
if (orgInvite != null) {
|
||||
request.organizationUserId = orgInvite.organizationUserId;
|
||||
request.token = orgInvite.token;
|
||||
}
|
||||
// Invite is accepted after login (on deep link redirect).
|
||||
};
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
|
@ -1,9 +1,30 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { Component, inject } from "@angular/core";
|
||||
|
||||
import { SetPasswordComponent as BaseSetPasswordComponent } from "@bitwarden/angular/auth/components/set-password.component";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
|
||||
|
||||
import { RouterService } from "../core";
|
||||
|
||||
import { AcceptOrganizationInviteService } from "./organization-invite/accept-organization.service";
|
||||
|
||||
@Component({
|
||||
selector: "app-set-password",
|
||||
templateUrl: "set-password.component.html",
|
||||
})
|
||||
export class SetPasswordComponent extends BaseSetPasswordComponent {}
|
||||
export class SetPasswordComponent extends BaseSetPasswordComponent {
|
||||
routerService = inject(RouterService);
|
||||
acceptOrganizationInviteService = inject(AcceptOrganizationInviteService);
|
||||
|
||||
protected override async onSetPasswordSuccess(
|
||||
masterKey: MasterKey,
|
||||
userKey: [UserKey, EncString],
|
||||
keyPair: [string, EncString],
|
||||
): Promise<void> {
|
||||
await super.onSetPasswordSuccess(masterKey, userKey, keyPair);
|
||||
// SSO JIT accepts org invites when setting their MP, meaning
|
||||
// we can clear the deep linked url for accepting it.
|
||||
await this.routerService.getAndClearLoginRedirectUrl();
|
||||
await this.acceptOrganizationInviteService.clearOrganizationInvitation();
|
||||
}
|
||||
}
|
||||
|
@ -12,15 +12,16 @@ import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { PlanType } from "@bitwarden/common/billing/enums";
|
||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
|
||||
import { RouterService } from "../../core";
|
||||
import { SharedModule } from "../../shared";
|
||||
import { AcceptOrganizationInviteService } from "../organization-invite/accept-organization.service";
|
||||
import { OrganizationInvite } from "../organization-invite/organization-invite";
|
||||
|
||||
import { TrialInitiationComponent } from "./trial-initiation.component";
|
||||
import { VerticalStepperComponent } from "./vertical-stepper/vertical-stepper.component";
|
||||
@ -36,12 +37,16 @@ describe("TrialInitiationComponent", () => {
|
||||
let stateServiceMock: MockProxy<StateService>;
|
||||
let policyApiServiceMock: MockProxy<PolicyApiServiceAbstraction>;
|
||||
let policyServiceMock: MockProxy<PolicyService>;
|
||||
let routerServiceMock: MockProxy<RouterService>;
|
||||
let acceptOrgInviteServiceMock: MockProxy<AcceptOrganizationInviteService>;
|
||||
|
||||
beforeEach(() => {
|
||||
// only define services directly that we want to mock return values in this component
|
||||
stateServiceMock = mock<StateService>();
|
||||
policyApiServiceMock = mock<PolicyApiServiceAbstraction>();
|
||||
policyServiceMock = mock<PolicyService>();
|
||||
routerServiceMock = mock<RouterService>();
|
||||
acceptOrgInviteServiceMock = mock<AcceptOrganizationInviteService>();
|
||||
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
@ -81,7 +86,11 @@ describe("TrialInitiationComponent", () => {
|
||||
},
|
||||
{
|
||||
provide: RouterService,
|
||||
useValue: mock<RouterService>(),
|
||||
useValue: routerServiceMock,
|
||||
},
|
||||
{
|
||||
provide: AcceptOrganizationInviteService,
|
||||
useValue: acceptOrgInviteServiceMock,
|
||||
},
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA], // Allows child components to be ignored (such as register component)
|
||||
@ -100,8 +109,8 @@ describe("TrialInitiationComponent", () => {
|
||||
|
||||
// These tests demonstrate mocking service calls
|
||||
describe("onInit() enforcedPolicyOptions", () => {
|
||||
it("should not set enforcedPolicyOptions if state service returns no invite", async () => {
|
||||
stateServiceMock.getOrganizationInvitation.mockReturnValueOnce(null);
|
||||
it("should not set enforcedPolicyOptions if there isn't an org invite in deep linked url", async () => {
|
||||
acceptOrgInviteServiceMock.getOrganizationInvite.mockResolvedValueOnce(null);
|
||||
// Need to recreate component with new service mock
|
||||
fixture = TestBed.createComponent(TrialInitiationComponent);
|
||||
component = fixture.componentInstance;
|
||||
@ -109,37 +118,31 @@ describe("TrialInitiationComponent", () => {
|
||||
|
||||
expect(component.enforcedPolicyOptions).toBe(undefined);
|
||||
});
|
||||
it("should set enforcedPolicyOptions if state service returns an invite", async () => {
|
||||
it("should set enforcedPolicyOptions if the deep linked url has an org invite", async () => {
|
||||
// Set up service method mocks
|
||||
stateServiceMock.getOrganizationInvitation.mockReturnValueOnce(
|
||||
Promise.resolve({
|
||||
organizationId: testOrgId,
|
||||
token: "token",
|
||||
email: "testEmail",
|
||||
organizationUserId: "123",
|
||||
}),
|
||||
);
|
||||
acceptOrgInviteServiceMock.getOrganizationInvite.mockResolvedValueOnce({
|
||||
organizationId: testOrgId,
|
||||
token: "token",
|
||||
email: "testEmail",
|
||||
organizationUserId: "123",
|
||||
} as OrganizationInvite);
|
||||
policyApiServiceMock.getPoliciesByToken.mockReturnValueOnce(
|
||||
Promise.resolve({
|
||||
data: [
|
||||
{
|
||||
id: "345",
|
||||
organizationId: testOrgId,
|
||||
type: 1,
|
||||
data: [
|
||||
{
|
||||
minComplexity: 4,
|
||||
minLength: 10,
|
||||
requireLower: null,
|
||||
requireNumbers: null,
|
||||
requireSpecial: null,
|
||||
requireUpper: null,
|
||||
},
|
||||
],
|
||||
enabled: true,
|
||||
Promise.resolve([
|
||||
{
|
||||
id: "345",
|
||||
organizationId: testOrgId,
|
||||
type: 1,
|
||||
data: {
|
||||
minComplexity: 4,
|
||||
minLength: 10,
|
||||
requireLower: null,
|
||||
requireNumbers: null,
|
||||
requireSpecial: null,
|
||||
requireUpper: null,
|
||||
},
|
||||
],
|
||||
} as ListResponse<PolicyResponse>),
|
||||
enabled: true,
|
||||
},
|
||||
] as Policy[]),
|
||||
);
|
||||
policyServiceMock.masterPasswordPolicyOptions$.mockReturnValue(
|
||||
of({
|
||||
|
@ -14,13 +14,14 @@ import { ProductType } from "@bitwarden/common/enums";
|
||||
import { ReferenceEventRequest } from "@bitwarden/common/models/request/reference-event.request";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
|
||||
import {
|
||||
OrganizationCreatedEvent,
|
||||
SubscriptionProduct,
|
||||
TrialOrganizationType,
|
||||
} from "../../billing/accounts/trial-initiation/trial-billing-step.component";
|
||||
import { AcceptOrganizationInviteService } from "../organization-invite/accept-organization.service";
|
||||
import { OrganizationInvite } from "../organization-invite/organization-invite";
|
||||
|
||||
import { RouterService } from "./../../core/router.service";
|
||||
import { VerticalStepperComponent } from "./vertical-stepper/vertical-stepper.component";
|
||||
@ -121,12 +122,12 @@ export class TrialInitiationComponent implements OnInit, OnDestroy {
|
||||
protected router: Router,
|
||||
private formBuilder: UntypedFormBuilder,
|
||||
private titleCasePipe: TitleCasePipe,
|
||||
private stateService: StateService,
|
||||
private logService: LogService,
|
||||
private policyApiService: PolicyApiServiceAbstraction,
|
||||
private policyService: PolicyService,
|
||||
private i18nService: I18nService,
|
||||
private routerService: RouterService,
|
||||
private acceptOrgInviteService: AcceptOrganizationInviteService,
|
||||
) {}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
@ -180,30 +181,10 @@ export class TrialInitiationComponent implements OnInit, OnDestroy {
|
||||
: "Password Manager trial from marketing website";
|
||||
});
|
||||
|
||||
const invite = await this.stateService.getOrganizationInvitation();
|
||||
if (invite != null) {
|
||||
try {
|
||||
const policies = await this.policyApiService.getPoliciesByToken(
|
||||
invite.organizationId,
|
||||
invite.token,
|
||||
invite.email,
|
||||
invite.organizationUserId,
|
||||
);
|
||||
if (policies.data != null) {
|
||||
this.policies = Policy.fromListResponse(policies);
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.policies != null) {
|
||||
this.policyService
|
||||
.masterPasswordPolicyOptions$(this.policies)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((enforcedPasswordPolicyOptions) => {
|
||||
this.enforcedPolicyOptions = enforcedPasswordPolicyOptions;
|
||||
});
|
||||
// If there's a deep linked org invite, use it to get the password policies
|
||||
const orgInvite = await this.acceptOrgInviteService.getOrganizationInvite();
|
||||
if (orgInvite != null) {
|
||||
await this.initPasswordPolicies(orgInvite);
|
||||
}
|
||||
|
||||
this.orgInfoFormGroup.controls.name.valueChanges
|
||||
@ -304,5 +285,31 @@ export class TrialInitiationComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
private async initPasswordPolicies(invite: OrganizationInvite): Promise<void> {
|
||||
if (invite == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.policies = await this.policyApiService.getPoliciesByToken(
|
||||
invite.organizationId,
|
||||
invite.token,
|
||||
invite.email,
|
||||
invite.organizationUserId,
|
||||
);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
|
||||
if (this.policies != null) {
|
||||
this.policyService
|
||||
.masterPasswordPolicyOptions$(this.policies)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((enforcedPasswordPolicyOptions) => {
|
||||
this.enforcedPolicyOptions = enforcedPasswordPolicyOptions;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected readonly SubscriptionProduct = SubscriptionProduct;
|
||||
}
|
||||
|
@ -1,60 +1,24 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { Component, inject } from "@angular/core";
|
||||
|
||||
import { UpdatePasswordComponent as BaseUpdatePasswordComponent } from "@bitwarden/angular/auth/components/update-password.component";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { RouterService } from "../core";
|
||||
|
||||
import { AcceptOrganizationInviteService } from "./organization-invite/accept-organization.service";
|
||||
|
||||
@Component({
|
||||
selector: "app-update-password",
|
||||
templateUrl: "update-password.component.html",
|
||||
})
|
||||
export class UpdatePasswordComponent extends BaseUpdatePasswordComponent {
|
||||
constructor(
|
||||
router: Router,
|
||||
i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||
policyService: PolicyService,
|
||||
cryptoService: CryptoService,
|
||||
messagingService: MessagingService,
|
||||
apiService: ApiService,
|
||||
logService: LogService,
|
||||
stateService: StateService,
|
||||
userVerificationService: UserVerificationService,
|
||||
dialogService: DialogService,
|
||||
kdfConfigService: KdfConfigService,
|
||||
masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
accountService: AccountService,
|
||||
) {
|
||||
super(
|
||||
router,
|
||||
i18nService,
|
||||
platformUtilsService,
|
||||
passwordGenerationService,
|
||||
policyService,
|
||||
cryptoService,
|
||||
messagingService,
|
||||
apiService,
|
||||
stateService,
|
||||
userVerificationService,
|
||||
logService,
|
||||
dialogService,
|
||||
kdfConfigService,
|
||||
masterPasswordService,
|
||||
accountService,
|
||||
);
|
||||
private routerService = inject(RouterService);
|
||||
private acceptOrganizationInviteService = inject(AcceptOrganizationInviteService);
|
||||
|
||||
override async cancel() {
|
||||
// clearing the login redirect url so that the user
|
||||
// does not join the organization if they cancel
|
||||
await this.routerService.getAndClearLoginRedirectUrl();
|
||||
await this.acceptOrganizationInviteService.clearOrganizationInvitation();
|
||||
await super.cancel();
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,12 @@
|
||||
import { Directive, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute, Params, Router } from "@angular/router";
|
||||
import { Subject } from "rxjs";
|
||||
import { Subject, firstValueFrom } from "rxjs";
|
||||
import { first, switchMap, takeUntil } from "rxjs/operators";
|
||||
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
|
||||
@Directive()
|
||||
export abstract class BaseAcceptComponent implements OnInit {
|
||||
@ -25,7 +26,7 @@ export abstract class BaseAcceptComponent implements OnInit {
|
||||
protected platformUtilService: PlatformUtilsService,
|
||||
protected i18nService: I18nService,
|
||||
protected route: ActivatedRoute,
|
||||
protected stateService: StateService,
|
||||
protected authService: AuthService,
|
||||
) {}
|
||||
|
||||
abstract authedHandler(qParams: Params): Promise<void>;
|
||||
@ -41,10 +42,10 @@ export abstract class BaseAcceptComponent implements OnInit {
|
||||
);
|
||||
let errorMessage: string = null;
|
||||
if (!error) {
|
||||
this.authed = await this.stateService.getIsAuthenticated();
|
||||
this.email = qParams.email;
|
||||
|
||||
if (this.authed) {
|
||||
const status = await firstValueFrom(this.authService.activeAccountStatus$);
|
||||
if (status !== AuthenticationStatus.LoggedOut) {
|
||||
try {
|
||||
await this.authedHandler(qParams);
|
||||
} catch (e) {
|
||||
|
@ -92,7 +92,7 @@ export class RouterService {
|
||||
/**
|
||||
* Fetch and clear persisted LoginRedirectUrl if present in state
|
||||
*/
|
||||
async getAndClearLoginRedirectUrl(): Promise<string> | undefined {
|
||||
async getAndClearLoginRedirectUrl(): Promise<string | undefined> {
|
||||
const persistedPreLoginUrl = await firstValueFrom(this.deepLinkRedirectUrlState.state$);
|
||||
|
||||
if (!Utils.isNullOrEmpty(persistedPreLoginUrl)) {
|
||||
|
@ -17,7 +17,6 @@ import { FamiliesForEnterpriseSetupComponent } from "./admin-console/organizatio
|
||||
import { VerifyRecoverDeleteProviderComponent } from "./admin-console/providers/verify-recover-delete-provider.component";
|
||||
import { CreateOrganizationComponent } from "./admin-console/settings/create-organization.component";
|
||||
import { SponsoredFamiliesComponent } from "./admin-console/settings/sponsored-families.component";
|
||||
import { AcceptOrganizationComponent } from "./auth/accept-organization.component";
|
||||
import { deepLinkGuard } from "./auth/guards/deep-link.guard";
|
||||
import { HintComponent } from "./auth/hint.component";
|
||||
import { LockComponent } from "./auth/lock.component";
|
||||
@ -25,6 +24,7 @@ import { LoginDecryptionOptionsComponent } from "./auth/login/login-decryption-o
|
||||
import { LoginViaAuthRequestComponent } from "./auth/login/login-via-auth-request.component";
|
||||
import { LoginViaWebAuthnComponent } from "./auth/login/login-via-webauthn/login-via-webauthn.component";
|
||||
import { LoginComponent } from "./auth/login/login.component";
|
||||
import { AcceptOrganizationComponent } from "./auth/organization-invite/accept-organization.component";
|
||||
import { RecoverDeleteComponent } from "./auth/recover-delete.component";
|
||||
import { RecoverTwoFactorComponent } from "./auth/recover-two-factor.component";
|
||||
import { RemovePasswordComponent } from "./auth/remove-password.component";
|
||||
@ -120,8 +120,8 @@ const routes: Routes = [
|
||||
{ path: "verify-email", component: VerifyEmailTokenComponent },
|
||||
{
|
||||
path: "accept-organization",
|
||||
component: AcceptOrganizationComponent,
|
||||
canActivate: [deepLinkGuard()],
|
||||
component: AcceptOrganizationComponent,
|
||||
data: { titleId: "joinOrganization", doNotSaveUrl: false },
|
||||
},
|
||||
{
|
||||
|
@ -20,7 +20,6 @@ import { ProvidersComponent } from "../admin-console/providers/providers.compone
|
||||
import { VerifyRecoverDeleteProviderComponent } from "../admin-console/providers/verify-recover-delete-provider.component";
|
||||
import { SponsoredFamiliesComponent } from "../admin-console/settings/sponsored-families.component";
|
||||
import { SponsoringOrgRowComponent } from "../admin-console/settings/sponsoring-org-row.component";
|
||||
import { AcceptOrganizationComponent } from "../auth/accept-organization.component";
|
||||
import { HintComponent } from "../auth/hint.component";
|
||||
import { LockComponent } from "../auth/lock.component";
|
||||
import { RecoverDeleteComponent } from "../auth/recover-delete.component";
|
||||
@ -120,7 +119,6 @@ import { SharedModule } from "./shared.module";
|
||||
],
|
||||
declarations: [
|
||||
AcceptFamilySponsorshipComponent,
|
||||
AcceptOrganizationComponent,
|
||||
AccountComponent,
|
||||
AddEditComponent,
|
||||
AddEditCustomFieldsComponent,
|
||||
@ -193,7 +191,6 @@ import { SharedModule } from "./shared.module";
|
||||
exports: [
|
||||
UserVerificationModule,
|
||||
PremiumBadgeComponent,
|
||||
AcceptOrganizationComponent,
|
||||
AccountComponent,
|
||||
AddEditComponent,
|
||||
AddEditCustomFieldsComponent,
|
||||
|
@ -3,9 +3,9 @@ import { ActivatedRoute, Params, Router } from "@angular/router";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { ProviderUserAcceptRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-accept.request";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { BaseAcceptComponent } from "@bitwarden/web-vault/app/common/base.accept.component";
|
||||
|
||||
@Component({
|
||||
@ -23,11 +23,11 @@ export class AcceptProviderComponent extends BaseAcceptComponent {
|
||||
router: Router,
|
||||
i18nService: I18nService,
|
||||
route: ActivatedRoute,
|
||||
stateService: StateService,
|
||||
authService: AuthService,
|
||||
private apiService: ApiService,
|
||||
platformUtilService: PlatformUtilsService,
|
||||
) {
|
||||
super(router, platformUtilService, i18nService, route, stateService);
|
||||
super(router, platformUtilService, i18nService, route, authService);
|
||||
}
|
||||
|
||||
async authedHandler(qParams: Params) {
|
||||
|
@ -155,7 +155,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
|
||||
|
||||
if (this.handleCaptchaRequired(response)) {
|
||||
return;
|
||||
} else if (this.handleMigrateEncryptionKey(response)) {
|
||||
} else if (await this.handleMigrateEncryptionKey(response)) {
|
||||
return;
|
||||
} else if (response.requiresTwoFactor) {
|
||||
if (this.onSuccessfulLoginTwoFactorNavigate != null) {
|
||||
@ -218,9 +218,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
|
||||
}
|
||||
|
||||
this.setLoginEmailValues();
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["/login-with-device"]);
|
||||
await this.router.navigate(["/login-with-device"]);
|
||||
}
|
||||
|
||||
async launchSsoBrowser(clientId: string, ssoRedirectUri: string) {
|
||||
@ -310,7 +308,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
|
||||
|
||||
// Legacy accounts used the master key to encrypt data. Migration is required
|
||||
// but only performed on web
|
||||
protected handleMigrateEncryptionKey(result: AuthResult): boolean {
|
||||
protected async handleMigrateEncryptionKey(result: AuthResult): Promise<boolean> {
|
||||
if (!result.requiresEncryptionKeyMigration) {
|
||||
return false;
|
||||
}
|
||||
|
@ -78,6 +78,10 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn
|
||||
|
||||
protected captchaBypassToken: string = null;
|
||||
|
||||
// allows for extending classes to modify the register request before sending
|
||||
// currently used by web to add organization invitation details
|
||||
protected modifyRegisterRequest: (request: RegisterRequest) => Promise<void>;
|
||||
|
||||
constructor(
|
||||
protected formValidationErrorService: FormValidationErrorsService,
|
||||
protected formBuilder: UntypedFormBuilder,
|
||||
@ -290,10 +294,8 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn
|
||||
kdfConfig.iterations,
|
||||
);
|
||||
request.keys = new KeysRequest(keys[0], keys[1].encryptedString);
|
||||
const orgInvite = await this.stateService.getOrganizationInvitation();
|
||||
if (orgInvite != null && orgInvite.token != null && orgInvite.organizationUserId != null) {
|
||||
request.token = orgInvite.token;
|
||||
request.organizationUserId = orgInvite.organizationUserId;
|
||||
if (this.modifyRegisterRequest) {
|
||||
await this.modifyRegisterRequest(request);
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
@ -72,10 +72,7 @@ export class UpdatePasswordComponent extends BaseChangePasswordComponent {
|
||||
}
|
||||
|
||||
async cancel() {
|
||||
await this.stateService.setOrganizationInvitation(null);
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["/vault"]);
|
||||
await this.router.navigate(["/vault"]);
|
||||
}
|
||||
|
||||
async setupSubmitActions(): Promise<boolean> {
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { ListResponse } from "../../../models/response/list.response";
|
||||
import { PolicyType } from "../../enums";
|
||||
import { MasterPasswordPolicyOptions } from "../../models/domain/master-password-policy-options";
|
||||
import { Policy } from "../../models/domain/policy";
|
||||
import { PolicyRequest } from "../../models/request/policy.request";
|
||||
import { PolicyResponse } from "../../models/response/policy.response";
|
||||
|
||||
@ -13,7 +14,7 @@ export class PolicyApiServiceAbstraction {
|
||||
token: string,
|
||||
email: string,
|
||||
organizationUserId: string,
|
||||
) => Promise<ListResponse<PolicyResponse>>;
|
||||
) => Promise<Policy[] | undefined>;
|
||||
|
||||
getMasterPasswordPolicyOptsForOrgUser: (orgId: string) => Promise<MasterPasswordPolicyOptions>;
|
||||
putPolicy: (organizationId: string, type: PolicyType, request: PolicyRequest) => Promise<any>;
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { PolicyId } from "../../../types/guid";
|
||||
import { PolicyType } from "../../enums";
|
||||
import { Policy } from "../domain/policy";
|
||||
import { PolicyResponse } from "../response/policy.response";
|
||||
|
||||
export class PolicyData {
|
||||
@ -20,4 +21,8 @@ export class PolicyData {
|
||||
this.data = response.data;
|
||||
this.enabled = response.enabled;
|
||||
}
|
||||
|
||||
static fromPolicy(policy: Policy): PolicyData {
|
||||
return Object.assign(new PolicyData(), policy);
|
||||
}
|
||||
}
|
||||
|
@ -47,7 +47,7 @@ export class PolicyApiService implements PolicyApiServiceAbstraction {
|
||||
token: string,
|
||||
email: string,
|
||||
organizationUserId: string,
|
||||
): Promise<ListResponse<PolicyResponse>> {
|
||||
): Promise<Policy[] | undefined> {
|
||||
const r = await this.apiService.send(
|
||||
"GET",
|
||||
"/organizations/" +
|
||||
@ -63,7 +63,7 @@ export class PolicyApiService implements PolicyApiServiceAbstraction {
|
||||
false,
|
||||
true,
|
||||
);
|
||||
return new ListResponse(r, PolicyResponse);
|
||||
return Policy.fromListResponse(new ListResponse(r, PolicyResponse));
|
||||
}
|
||||
|
||||
private async getMasterPasswordPolicyResponseForOrgUser(
|
||||
|
@ -3,11 +3,15 @@ import { UserKey } from "../../types/key";
|
||||
export abstract class PasswordResetEnrollmentServiceAbstraction {
|
||||
/*
|
||||
* Checks the user's enrollment status and enrolls them if required
|
||||
* NOTE: Will also enroll the user in the organization if in the
|
||||
* invited status
|
||||
*/
|
||||
abstract enrollIfRequired(organizationSsoIdentifier: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Enroll current user in password reset
|
||||
* NOTE: Will also enroll the user in the organization if in the
|
||||
* invited status
|
||||
* @param organizationId - Organization in which to enroll the user
|
||||
* @returns Promise that resolves when the user is enrolled
|
||||
* @throws Error if the action fails
|
||||
@ -16,6 +20,8 @@ export abstract class PasswordResetEnrollmentServiceAbstraction {
|
||||
|
||||
/**
|
||||
* Enroll user in password reset
|
||||
* NOTE: Will also enroll the user in the organization if in the
|
||||
* invited status
|
||||
* @param organizationId - Organization in which to enroll the user
|
||||
* @param userId - User to enroll
|
||||
* @param userKey - User's symmetric key
|
||||
|
@ -101,8 +101,6 @@ export abstract class StateService<T extends Account = Account> {
|
||||
setLastSync: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getMinimizeOnCopyToClipboard: (options?: StorageOptions) => Promise<boolean>;
|
||||
setMinimizeOnCopyToClipboard: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getOrganizationInvitation: (options?: StorageOptions) => Promise<any>;
|
||||
setOrganizationInvitation: (value: any, options?: StorageOptions) => Promise<void>;
|
||||
getPasswordGenerationOptions: (options?: StorageOptions) => Promise<PasswordGeneratorOptions>;
|
||||
setPasswordGenerationOptions: (
|
||||
value: PasswordGeneratorOptions,
|
||||
|
@ -1,5 +1,4 @@
|
||||
export class GlobalState {
|
||||
organizationInvitation?: any;
|
||||
enableBrowserIntegration?: boolean;
|
||||
enableBrowserIntegrationFingerprint?: boolean;
|
||||
enableDuckDuckGoBrowserIntegration?: boolean;
|
||||
|
@ -474,23 +474,6 @@ export class StateService<
|
||||
);
|
||||
}
|
||||
|
||||
async getOrganizationInvitation(options?: StorageOptions): Promise<any> {
|
||||
return (
|
||||
await this.getGlobals(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
|
||||
)?.organizationInvitation;
|
||||
}
|
||||
|
||||
async setOrganizationInvitation(value: any, options?: StorageOptions): Promise<void> {
|
||||
const globals = await this.getGlobals(
|
||||
this.reconcileOptions(options, await this.defaultInMemoryOptions()),
|
||||
);
|
||||
globals.organizationInvitation = value;
|
||||
await this.saveGlobals(
|
||||
globals,
|
||||
this.reconcileOptions(options, await this.defaultInMemoryOptions()),
|
||||
);
|
||||
}
|
||||
|
||||
async getPasswordGenerationOptions(options?: StorageOptions): Promise<PasswordGeneratorOptions> {
|
||||
return (
|
||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()))
|
||||
|
@ -63,6 +63,7 @@ export const TOKEN_DISK_LOCAL = new StateDefinition("tokenDiskLocal", "disk", {
|
||||
export const TOKEN_MEMORY = new StateDefinition("token", "memory");
|
||||
export const TWO_FACTOR_MEMORY = new StateDefinition("twoFactor", "memory");
|
||||
export const USER_DECRYPTION_OPTIONS_DISK = new StateDefinition("userDecryptionOptions", "disk");
|
||||
export const ORGANIZATION_INVITE_DISK = new StateDefinition("organizationInvite", "disk");
|
||||
export const VAULT_TIMEOUT_SETTINGS_DISK_LOCAL = new StateDefinition(
|
||||
"vaultTimeoutSettings",
|
||||
"disk",
|
||||
|
Loading…
Reference in New Issue
Block a user