1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-06 10:33:57 +00:00
Files
browser/apps/web/src/app/auth/organization-invite/accept-organization.service.ts
Jake Fink d7bf0fe536 [PM-8868] only deserialize org invite if not nullish (#9644)
* only deserialize org invite if not nullish

* add null check to OrganizationInvite init methods

* PR feedback
2024-06-25 15:00:27 -04:00

249 lines
9.5 KiB
TypeScript

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 | null>(
ORGANIZATION_INVITE_DISK,
"organizationInvite",
{
deserializer: (invite) => (invite ? OrganizationInvite.fromJSON(invite) : null),
},
);
@Injectable()
export class AcceptOrganizationInviteService {
private organizationInvitationState: GlobalState<OrganizationInvite | null>;
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 | null> {
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;
}
}