From 735a114baaae2023a7348176caa2adfcb2135a7b Mon Sep 17 00:00:00 2001 From: Patrick Pimentel Date: Sun, 22 Jun 2025 20:31:20 -0400 Subject: [PATCH] fix(change-password-component): Change Password Update [18720] - Took org invite state out of service and made it accessible. --- .../web-login-decryption-options.service.ts | 6 +- .../login/web-login-component.service.ts | 13 +-- .../web-registration-finish.service.spec.ts | 52 +++++------- .../web-registration-finish.service.ts | 13 ++- .../web-set-password-jit.service.ts | 37 ++++++++- .../accept-organization.component.ts | 37 +++++++-- .../accept-organization.service.spec.ts | 17 ++-- .../accept-organization.service.ts | 62 ++------------ .../organization-invite.ts | 40 --------- .../src/app/auth/set-password.component.ts | 7 +- .../settings/change-password.component.ts | 30 +++---- .../emergency-access-takeover.component.ts | 8 +- .../src/app/auth/update-password.component.ts | 7 +- .../complete-trial-initiation.component.ts | 6 +- apps/web/src/app/core/core.module.ts | 15 ++-- .../components/change-password.component.ts | 8 +- .../auth/components/set-password.component.ts | 42 +++++----- .../components/update-password.component.ts | 8 +- .../update-temp-password.component.ts | 8 +- libs/angular/src/auth/guards/auth.guard.ts | 33 +++++--- .../src/services/jslib-services.module.ts | 5 +- .../change-password.component.ts | 11 +-- .../auth/src/angular/login/login.component.ts | 14 +++- .../new-device-verification.component.ts | 34 +++++++- .../default-set-password-jit.service.ts | 6 +- .../two-factor-auth.component.ts | 22 +++++ .../password-login.strategy.ts | 15 ++-- .../policy/policy.service.abstraction.ts | 15 +++- .../domain/master-password-policy-options.ts | 11 +-- .../services/policy/default-policy.service.ts | 82 +++++++++++-------- .../organization-invite-service.ts | 37 +++++++++ .../organization-invite-state.ts | 13 +++ .../organization-invite.ts | 20 +++++ 33 files changed, 417 insertions(+), 317 deletions(-) delete mode 100644 apps/web/src/app/auth/organization-invite/organization-invite.ts create mode 100644 libs/common/src/auth/services/organization-invite/organization-invite-service.ts create mode 100644 libs/common/src/auth/services/organization-invite/organization-invite-state.ts create mode 100644 libs/common/src/auth/services/organization-invite/organization-invite.ts diff --git a/apps/web/src/app/auth/core/services/login-decryption-options/web-login-decryption-options.service.ts b/apps/web/src/app/auth/core/services/login-decryption-options/web-login-decryption-options.service.ts index 3de3ec46457..ddd4caf54b8 100644 --- a/apps/web/src/app/auth/core/services/login-decryption-options/web-login-decryption-options.service.ts +++ b/apps/web/src/app/auth/core/services/login-decryption-options/web-login-decryption-options.service.ts @@ -4,10 +4,10 @@ import { LoginDecryptionOptionsService, DefaultLoginDecryptionOptionsService, } from "@bitwarden/auth/angular"; +import { OrganizationInviteService } from "@bitwarden/common/auth/services/organization-invite/organization-invite-service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { RouterService } from "../../../../core/router.service"; -import { AcceptOrganizationInviteService } from "../../../organization-invite/accept-organization.service"; export class WebLoginDecryptionOptionsService extends DefaultLoginDecryptionOptionsService @@ -16,7 +16,7 @@ export class WebLoginDecryptionOptionsService constructor( protected messagingService: MessagingService, private routerService: RouterService, - private acceptOrganizationInviteService: AcceptOrganizationInviteService, + private organizationInviteService: OrganizationInviteService, ) { super(messagingService); } @@ -27,7 +27,7 @@ export class WebLoginDecryptionOptionsService // 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.organizationInviteService.clearOrganizationInvitation(); } catch (error) { throw new Error(error); } diff --git a/apps/web/src/app/auth/core/services/login/web-login-component.service.ts b/apps/web/src/app/auth/core/services/login/web-login-component.service.ts index a9d7de1c562..a14bfb4d436 100644 --- a/apps/web/src/app/auth/core/services/login/web-login-component.service.ts +++ b/apps/web/src/app/auth/core/services/login/web-login-component.service.ts @@ -16,6 +16,7 @@ import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { OrganizationInviteService } from "@bitwarden/common/auth/services/organization-invite/organization-invite-service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -25,7 +26,6 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { RouterService } from "../../../../core/router.service"; -import { AcceptOrganizationInviteService } from "../../../organization-invite/accept-organization.service"; @Injectable() export class WebLoginComponentService @@ -33,7 +33,7 @@ export class WebLoginComponentService implements LoginComponentService { constructor( - protected acceptOrganizationInviteService: AcceptOrganizationInviteService, + protected organizationInviteService: OrganizationInviteService, protected logService: LogService, protected policyApiService: PolicyApiServiceAbstraction, protected policyService: InternalPolicyService, @@ -70,8 +70,8 @@ export class WebLoginComponentService return; } - async getOrgPoliciesFromOrgInvite(): Promise { - const orgInvite = await this.acceptOrganizationInviteService.getOrganizationInvite(); + async getOrgPoliciesFromOrgInvite(): Promise { + const orgInvite = await this.organizationInviteService.getOrganizationInvite(); if (orgInvite != null) { let policies: Policy[]; @@ -88,7 +88,7 @@ export class WebLoginComponentService } if (policies == null) { - return; + return undefined; } const resetPasswordPolicy = this.policyService.getResetPasswordPolicyOptions( @@ -104,7 +104,8 @@ export class WebLoginComponentService if ( await this.configService.getFeatureFlag(FeatureFlag.PM16117_ChangeExistingPasswordRefactor) ) { - enforcedPasswordPolicyOptions = this.policyService.combineMasterPasswordPolicies(policies); + enforcedPasswordPolicyOptions = + this.policyService.combinePoliciesIntoMasterPasswordPolicyOptions(policies); } else { enforcedPasswordPolicyOptions = await firstValueFrom( this.accountService.activeAccount$.pipe( diff --git a/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.spec.ts b/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.spec.ts index fe3b0837aa8..f3e71ea7998 100644 --- a/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.spec.ts +++ b/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.spec.ts @@ -9,19 +9,15 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-api.service"; +import { OrganizationInvite } from "@bitwarden/common/auth/services/organization-invite/organization-invite"; +import { OrganizationInviteService } from "@bitwarden/common/auth/services/organization-invite/organization-invite-service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; -import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { CsprngArray } from "@bitwarden/common/types/csprng"; -import { UserId } from "@bitwarden/common/types/guid"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; import { DEFAULT_KDF_CONFIG, KeyService } from "@bitwarden/key-management"; -import { AcceptOrganizationInviteService } from "../../../organization-invite/accept-organization.service"; -import { OrganizationInvite } from "../../../organization-invite/organization-invite"; - import { WebRegistrationFinishService } from "./web-registration-finish.service"; describe("WebRegistrationFinishService", () => { @@ -29,30 +25,26 @@ describe("WebRegistrationFinishService", () => { let keyService: MockProxy; let accountApiService: MockProxy; - let acceptOrgInviteService: MockProxy; + let organizationInviteService: MockProxy; let policyApiService: MockProxy; let logService: MockProxy; let policyService: MockProxy; - const mockUserId = Utils.newGuid() as UserId; - let accountService: FakeAccountService; beforeEach(() => { keyService = mock(); accountApiService = mock(); - acceptOrgInviteService = mock(); + organizationInviteService = mock(); policyApiService = mock(); logService = mock(); policyService = mock(); - accountService = mockAccountServiceWith(mockUserId); service = new WebRegistrationFinishService( keyService, accountApiService, - acceptOrgInviteService, + organizationInviteService, policyApiService, logService, policyService, - accountService, ); }); @@ -72,21 +64,21 @@ describe("WebRegistrationFinishService", () => { }); it("returns null when the org invite is null", async () => { - acceptOrgInviteService.getOrganizationInvite.mockResolvedValue(null); + organizationInviteService.getOrganizationInvite.mockResolvedValue(null); const result = await service.getOrgNameFromOrgInvite(); expect(result).toBeNull(); - expect(acceptOrgInviteService.getOrganizationInvite).toHaveBeenCalled(); + expect(organizationInviteService.getOrganizationInvite).toHaveBeenCalled(); }); it("returns the organization name from the organization invite when it exists", async () => { - acceptOrgInviteService.getOrganizationInvite.mockResolvedValue(orgInvite); + organizationInviteService.getOrganizationInvite.mockResolvedValue(orgInvite); const result = await service.getOrgNameFromOrgInvite(); expect(result).toEqual(orgInvite.organizationName); - expect(acceptOrgInviteService.getOrganizationInvite).toHaveBeenCalled(); + expect(organizationInviteService.getOrganizationInvite).toHaveBeenCalled(); }); }); @@ -102,22 +94,22 @@ describe("WebRegistrationFinishService", () => { }); it("returns null when the org invite is null", async () => { - acceptOrgInviteService.getOrganizationInvite.mockResolvedValue(null); + organizationInviteService.getOrganizationInvite.mockResolvedValue(null); const result = await service.getMasterPasswordPolicyOptsFromOrgInvite(); expect(result).toBeNull(); - expect(acceptOrgInviteService.getOrganizationInvite).toHaveBeenCalled(); + expect(organizationInviteService.getOrganizationInvite).toHaveBeenCalled(); }); it("returns null when the policies are null", async () => { - acceptOrgInviteService.getOrganizationInvite.mockResolvedValue(orgInvite); + organizationInviteService.getOrganizationInvite.mockResolvedValue(orgInvite); policyApiService.getPoliciesByToken.mockResolvedValue(null); const result = await service.getMasterPasswordPolicyOptsFromOrgInvite(); expect(result).toBeNull(); - expect(acceptOrgInviteService.getOrganizationInvite).toHaveBeenCalled(); + expect(organizationInviteService.getOrganizationInvite).toHaveBeenCalled(); expect(policyApiService.getPoliciesByToken).toHaveBeenCalledWith( orgInvite.organizationId, orgInvite.token, @@ -127,13 +119,13 @@ describe("WebRegistrationFinishService", () => { }); it("logs an error and returns null when policies cannot be fetched", async () => { - acceptOrgInviteService.getOrganizationInvite.mockResolvedValue(orgInvite); + organizationInviteService.getOrganizationInvite.mockResolvedValue(orgInvite); policyApiService.getPoliciesByToken.mockRejectedValue(new Error("error")); const result = await service.getMasterPasswordPolicyOptsFromOrgInvite(); expect(result).toBeNull(); - expect(acceptOrgInviteService.getOrganizationInvite).toHaveBeenCalled(); + expect(organizationInviteService.getOrganizationInvite).toHaveBeenCalled(); expect(policyApiService.getPoliciesByToken).toHaveBeenCalledWith( orgInvite.organizationId, orgInvite.token, @@ -147,14 +139,14 @@ describe("WebRegistrationFinishService", () => { const masterPasswordPolicies = [new Policy()]; const masterPasswordPolicyOptions = new MasterPasswordPolicyOptions(); - acceptOrgInviteService.getOrganizationInvite.mockResolvedValue(orgInvite); + organizationInviteService.getOrganizationInvite.mockResolvedValue(orgInvite); policyApiService.getPoliciesByToken.mockResolvedValue(masterPasswordPolicies); policyService.masterPasswordPolicyOptions$.mockReturnValue(of(masterPasswordPolicyOptions)); const result = await service.getMasterPasswordPolicyOptsFromOrgInvite(); expect(result).toEqual(masterPasswordPolicyOptions); - expect(acceptOrgInviteService.getOrganizationInvite).toHaveBeenCalled(); + expect(organizationInviteService.getOrganizationInvite).toHaveBeenCalled(); expect(policyApiService.getPoliciesByToken).toHaveBeenCalledWith( orgInvite.organizationId, orgInvite.token, @@ -221,7 +213,7 @@ describe("WebRegistrationFinishService", () => { keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]); keyService.makeKeyPair.mockResolvedValue(userKeyPair); accountApiService.registerFinish.mockResolvedValue(); - acceptOrgInviteService.getOrganizationInvite.mockResolvedValue(null); + organizationInviteService.getOrganizationInvite.mockResolvedValue(null); await service.finishRegistration(email, passwordInputResult, emailVerificationToken); @@ -257,7 +249,7 @@ describe("WebRegistrationFinishService", () => { keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]); keyService.makeKeyPair.mockResolvedValue(userKeyPair); accountApiService.registerFinish.mockResolvedValue(); - acceptOrgInviteService.getOrganizationInvite.mockResolvedValue(orgInvite); + organizationInviteService.getOrganizationInvite.mockResolvedValue(orgInvite); await service.finishRegistration(email, passwordInputResult); @@ -293,7 +285,7 @@ describe("WebRegistrationFinishService", () => { keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]); keyService.makeKeyPair.mockResolvedValue(userKeyPair); accountApiService.registerFinish.mockResolvedValue(); - acceptOrgInviteService.getOrganizationInvite.mockResolvedValue(null); + organizationInviteService.getOrganizationInvite.mockResolvedValue(null); await service.finishRegistration( email, @@ -334,7 +326,7 @@ describe("WebRegistrationFinishService", () => { keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]); keyService.makeKeyPair.mockResolvedValue(userKeyPair); accountApiService.registerFinish.mockResolvedValue(); - acceptOrgInviteService.getOrganizationInvite.mockResolvedValue(null); + organizationInviteService.getOrganizationInvite.mockResolvedValue(null); await service.finishRegistration( email, @@ -377,7 +369,7 @@ describe("WebRegistrationFinishService", () => { keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]); keyService.makeKeyPair.mockResolvedValue(userKeyPair); accountApiService.registerFinish.mockResolvedValue(); - acceptOrgInviteService.getOrganizationInvite.mockResolvedValue(null); + organizationInviteService.getOrganizationInvite.mockResolvedValue(null); await service.finishRegistration( email, diff --git a/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.ts b/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.ts index 3d99b3b6712..fc2bd010246 100644 --- a/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.ts +++ b/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.ts @@ -12,14 +12,12 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-api.service"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { RegisterFinishRequest } from "@bitwarden/common/auth/models/request/registration/register-finish.request"; +import { OrganizationInviteService } from "@bitwarden/common/auth/services/organization-invite/organization-invite-service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { KeyService } from "@bitwarden/key-management"; -import { AcceptOrganizationInviteService } from "../../../organization-invite/accept-organization.service"; - export class WebRegistrationFinishService extends DefaultRegistrationFinishService implements RegistrationFinishService @@ -27,17 +25,16 @@ export class WebRegistrationFinishService constructor( protected keyService: KeyService, protected accountApiService: AccountApiService, - private acceptOrgInviteService: AcceptOrganizationInviteService, + private organizationInviteService: OrganizationInviteService, private policyApiService: PolicyApiServiceAbstraction, private logService: LogService, private policyService: PolicyService, - private accountService: AccountService, ) { super(keyService, accountApiService); } override async getOrgNameFromOrgInvite(): Promise { - const orgInvite = await this.acceptOrgInviteService.getOrganizationInvite(); + const orgInvite = await this.organizationInviteService.getOrganizationInvite(); if (orgInvite == null) { return null; } @@ -47,7 +44,7 @@ export class WebRegistrationFinishService override async getMasterPasswordPolicyOptsFromOrgInvite(): Promise { // If there's a deep linked org invite, use it to get the password policies - const orgInvite = await this.acceptOrgInviteService.getOrganizationInvite(); + const orgInvite = await this.organizationInviteService.getOrganizationInvite(); if (orgInvite == null) { return null; @@ -100,7 +97,7 @@ export class WebRegistrationFinishService // web specific logic // 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 this.acceptOrgInviteService.getOrganizationInvite(); + const orgInvite = await this.organizationInviteService.getOrganizationInvite(); if (orgInvite != null) { registerRequest.organizationUserId = orgInvite.organizationUserId; registerRequest.orgInviteToken = orgInvite.token; diff --git a/apps/web/src/app/auth/core/services/set-password-jit/web-set-password-jit.service.ts b/apps/web/src/app/auth/core/services/set-password-jit/web-set-password-jit.service.ts index 62175f1256d..355d12a1843 100644 --- a/apps/web/src/app/auth/core/services/set-password-jit/web-set-password-jit.service.ts +++ b/apps/web/src/app/auth/core/services/set-password-jit/web-set-password-jit.service.ts @@ -1,20 +1,51 @@ import { inject } from "@angular/core"; +import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; import { DefaultSetPasswordJitService, SetPasswordCredentials, SetPasswordJitService, } from "@bitwarden/auth/angular"; +import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; +import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; +import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction"; +import { OrganizationInviteService } from "@bitwarden/common/auth/services/organization-invite/organization-invite-service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { KdfConfigService, KeyService } from "@bitwarden/key-management"; import { RouterService } from "../../../../core/router.service"; -import { AcceptOrganizationInviteService } from "../../../organization-invite/accept-organization.service"; export class WebSetPasswordJitService extends DefaultSetPasswordJitService implements SetPasswordJitService { routerService = inject(RouterService); - acceptOrganizationInviteService = inject(AcceptOrganizationInviteService); + constructor( + protected encryptService: EncryptService, + protected i18nService: I18nService, + protected kdfConfigService: KdfConfigService, + protected keyService: KeyService, + protected masterPasswordApiService: MasterPasswordApiService, + protected masterPasswordService: InternalMasterPasswordServiceAbstraction, + protected organizationApiService: OrganizationApiServiceAbstraction, + protected organizationUserApiService: OrganizationUserApiService, + protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, + private organizationInviteService: OrganizationInviteService, + ) { + super( + encryptService, + i18nService, + kdfConfigService, + keyService, + masterPasswordApiService, + masterPasswordService, + organizationApiService, + organizationUserApiService, + userDecryptionOptionsService, + ); + } override async setPassword(credentials: SetPasswordCredentials) { await super.setPassword(credentials); @@ -22,6 +53,6 @@ export class WebSetPasswordJitService // 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(); + await this.organizationInviteService.clearOrganizationInvitation(); } } diff --git a/apps/web/src/app/auth/organization-invite/accept-organization.component.ts b/apps/web/src/app/auth/organization-invite/accept-organization.component.ts index 838a3029711..e59b02a3a6e 100644 --- a/apps/web/src/app/auth/organization-invite/accept-organization.component.ts +++ b/apps/web/src/app/auth/organization-invite/accept-organization.component.ts @@ -4,13 +4,14 @@ import { Component } from "@angular/core"; import { ActivatedRoute, Params, Router } from "@angular/router"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { OrganizationInvite } from "@bitwarden/common/auth/services/organization-invite/organization-invite"; +import { OrganizationInviteService } from "@bitwarden/common/auth/services/organization-invite/organization-invite-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", @@ -21,18 +22,19 @@ export class AcceptOrganizationComponent extends BaseAcceptComponent { protected requiredParameters: string[] = ["organizationId", "organizationUserId", "token"]; constructor( - router: Router, - platformUtilsService: PlatformUtilsService, - i18nService: I18nService, - route: ActivatedRoute, - authService: AuthService, + protected router: Router, + protected platformUtilsService: PlatformUtilsService, + protected i18nService: I18nService, + protected route: ActivatedRoute, + protected authService: AuthService, private acceptOrganizationInviteService: AcceptOrganizationInviteService, + private organizationInviteService: OrganizationInviteService, ) { super(router, platformUtilsService, i18nService, route, authService); } async authedHandler(qParams: Params): Promise { - const invite = OrganizationInvite.fromParams(qParams); + const invite = this.fromParams(qParams); const success = await this.acceptOrganizationInviteService.validateAndAcceptInvite(invite); if (!success) { @@ -52,9 +54,9 @@ export class AcceptOrganizationComponent extends BaseAcceptComponent { } async unauthedHandler(qParams: Params): Promise { - const invite = OrganizationInvite.fromParams(qParams); + const invite = this.fromParams(qParams); - await this.acceptOrganizationInviteService.setOrganizationInvitation(invite); + await this.organizationInviteService.setOrganizationInvitation(invite); await this.navigateInviteAcceptance(invite); } @@ -94,4 +96,21 @@ export class AcceptOrganizationComponent extends BaseAcceptComponent { }); return; } + + private fromParams(params: Params): OrganizationInvite | null { + if (params == null) { + return null; + } + + 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, + }); + } } diff --git a/apps/web/src/app/auth/organization-invite/accept-organization.service.spec.ts b/apps/web/src/app/auth/organization-invite/accept-organization.service.spec.ts index 253328b0c04..428348b98c1 100644 --- a/apps/web/src/app/auth/organization-invite/accept-organization.service.spec.ts +++ b/apps/web/src/app/auth/organization-invite/accept-organization.service.spec.ts @@ -15,22 +15,20 @@ import { ResetPasswordPolicyOptions } from "@bitwarden/common/admin-console/mode import { OrganizationKeysResponse } from "@bitwarden/common/admin-console/models/response/organization-keys.response"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { OrganizationInvite } from "@bitwarden/common/auth/services/organization-invite/organization-invite"; +import { OrganizationInviteService } from "@bitwarden/common/auth/services/organization-invite/organization-invite-service"; +import { ORGANIZATION_INVITE } from "@bitwarden/common/auth/services/organization-invite/organization-invite-state"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; 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 { DialogService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; import { I18nService } from "../../core/i18n.service"; -import { - AcceptOrganizationInviteService, - ORGANIZATION_INVITE, -} from "./accept-organization.service"; -import { OrganizationInvite } from "./organization-invite"; +import { AcceptOrganizationInviteService } from "./accept-organization.service"; describe("AcceptOrganizationInviteService", () => { let sut: AcceptOrganizationInviteService; @@ -43,10 +41,10 @@ describe("AcceptOrganizationInviteService", () => { let logService: MockProxy; let organizationApiService: MockProxy; let organizationUserApiService: MockProxy; + let organizationInviteService: MockProxy; let i18nService: MockProxy; let globalStateProvider: FakeGlobalStateProvider; let globalState: FakeGlobalState; - let dialogService: MockProxy; let accountService: MockProxy; beforeEach(() => { @@ -59,10 +57,10 @@ describe("AcceptOrganizationInviteService", () => { logService = mock(); organizationApiService = mock(); organizationUserApiService = mock(); + organizationInviteService = mock(); i18nService = mock(); globalStateProvider = new FakeGlobalStateProvider(); globalState = globalStateProvider.getFake(ORGANIZATION_INVITE); - dialogService = mock(); accountService = mock(); sut = new AcceptOrganizationInviteService( @@ -76,8 +74,7 @@ describe("AcceptOrganizationInviteService", () => { organizationApiService, organizationUserApiService, i18nService, - globalStateProvider, - dialogService, + organizationInviteService, accountService, ); }); diff --git a/apps/web/src/app/auth/organization-invite/accept-organization.service.ts b/apps/web/src/app/auth/organization-invite/accept-organization.service.ts index c68b174166d..578d7f88dde 100644 --- a/apps/web/src/app/auth/organization-invite/accept-organization.service.ts +++ b/apps/web/src/app/auth/organization-invite/accept-organization.service.ts @@ -17,36 +17,17 @@ import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { OrganizationInvite } from "@bitwarden/common/auth/services/organization-invite/organization-invite"; +import { OrganizationInviteService } from "@bitwarden/common/auth/services/organization-invite/organization-invite-service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/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 { DialogService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; -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( - ORGANIZATION_INVITE_DISK, - "organizationInvite", - { - deserializer: (invite) => (invite ? OrganizationInvite.fromJSON(invite) : null), - }, -); - @Injectable() export class AcceptOrganizationInviteService { - private organizationInvitationState: GlobalState; private orgNameSubject: BehaviorSubject = new BehaviorSubject(null); private policyCache: Policy[]; @@ -64,34 +45,9 @@ export class AcceptOrganizationInviteService { private readonly organizationApiService: OrganizationApiServiceAbstraction, private readonly organizationUserApiService: OrganizationUserApiService, private readonly i18nService: I18nService, - private readonly globalStateProvider: GlobalStateProvider, - private readonly dialogService: DialogService, + private readonly organizationInviteService: OrganizationInviteService, private readonly accountService: AccountService, - ) { - this.organizationInvitationState = this.globalStateProvider.get(ORGANIZATION_INVITE); - } - - /** Returns the currently stored organization invite */ - async getOrganizationInvite(): Promise { - 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 { - 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 { - await this.organizationInvitationState.update(() => null); - } + ) {} /** * Validates and accepts the organization invitation if possible. @@ -113,7 +69,7 @@ export class AcceptOrganizationInviteService { // Accepting an org invite from existing org if (await this.masterPasswordPolicyCheckRequired(invite)) { - await this.setOrganizationInvitation(invite); + await this.organizationInviteService.setOrganizationInvitation(invite); this.authService.logOut(() => { /* Do nothing */ }); @@ -134,7 +90,7 @@ export class AcceptOrganizationInviteService { ), ); await this.apiService.refreshIdentityToken(); - await this.clearOrganizationInvitation(); + await this.organizationInviteService.clearOrganizationInvitation(); } private async prepareAcceptAndInitRequest( @@ -170,7 +126,7 @@ export class AcceptOrganizationInviteService { ); await this.apiService.refreshIdentityToken(); - await this.clearOrganizationInvitation(); + await this.organizationInviteService.clearOrganizationInvitation(); } private async prepareAcceptRequest( @@ -224,10 +180,10 @@ export class AcceptOrganizationInviteService { (p) => p.type === PolicyType.MasterPassword && p.enabled, ); - let storedInvite = await this.getOrganizationInvite(); + let storedInvite = await this.organizationInviteService.getOrganizationInvite(); if (storedInvite?.email !== invite.email) { // clear stored invites if the email doesn't match - await this.clearOrganizationInvitation(); + await this.organizationInviteService.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 diff --git a/apps/web/src/app/auth/organization-invite/organization-invite.ts b/apps/web/src/app/auth/organization-invite/organization-invite.ts deleted file mode 100644 index 65414113e74..00000000000 --- a/apps/web/src/app/auth/organization-invite/organization-invite.ts +++ /dev/null @@ -1,40 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -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 | null { - if (json == null) { - return null; - } - - return Object.assign(new OrganizationInvite(), json); - } - - static fromParams(params: Params): OrganizationInvite | null { - if (params == null) { - return null; - } - - 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, - }); - } -} diff --git a/apps/web/src/app/auth/set-password.component.ts b/apps/web/src/app/auth/set-password.component.ts index e297426f2c1..df6d474be21 100644 --- a/apps/web/src/app/auth/set-password.component.ts +++ b/apps/web/src/app/auth/set-password.component.ts @@ -1,13 +1,12 @@ import { Component, inject } from "@angular/core"; import { SetPasswordComponent as BaseSetPasswordComponent } from "@bitwarden/angular/auth/components/set-password.component"; +import { OrganizationInviteService } from "@bitwarden/common/auth/services/organization-invite/organization-invite-service"; 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", @@ -15,7 +14,7 @@ import { AcceptOrganizationInviteService } from "./organization-invite/accept-or }) export class SetPasswordComponent extends BaseSetPasswordComponent { routerService = inject(RouterService); - acceptOrganizationInviteService = inject(AcceptOrganizationInviteService); + organizationInviteService = inject(OrganizationInviteService); protected override async onSetPasswordSuccess( masterKey: MasterKey, @@ -26,6 +25,6 @@ export class SetPasswordComponent extends BaseSetPasswordComponent { // 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(); + await this.organizationInviteService.clearOrganizationInvitation(); } } diff --git a/apps/web/src/app/auth/settings/change-password.component.ts b/apps/web/src/app/auth/settings/change-password.component.ts index 8faeee16cd2..ce10a0e5a34 100644 --- a/apps/web/src/app/auth/settings/change-password.component.ts +++ b/apps/web/src/app/auth/settings/change-password.component.ts @@ -43,34 +43,34 @@ export class ChangePasswordComponent characterMinimumMessage = ""; constructor( - i18nService: I18nService, - keyService: KeyService, - messagingService: MessagingService, - platformUtilsService: PlatformUtilsService, - policyService: PolicyService, private auditService: AuditService, private cipherService: CipherService, - private syncService: SyncService, + private keyRotationService: UserKeyRotationService, private masterPasswordApiService: MasterPasswordApiService, private router: Router, - dialogService: DialogService, + private syncService: SyncService, private userVerificationService: UserVerificationService, - private keyRotationService: UserKeyRotationService, - kdfConfigService: KdfConfigService, + protected accountService: AccountService, + protected dialogService: DialogService, + protected i18nService: I18nService, + protected kdfConfigService: KdfConfigService, + protected keyService: KeyService, protected masterPasswordService: InternalMasterPasswordServiceAbstraction, - accountService: AccountService, - toastService: ToastService, + protected messagingService: MessagingService, + protected platformUtilsService: PlatformUtilsService, + protected policyService: PolicyService, + protected toastService: ToastService, ) { super( + accountService, + dialogService, i18nService, + kdfConfigService, keyService, + masterPasswordService, messagingService, platformUtilsService, policyService, - dialogService, - kdfConfigService, - masterPasswordService, - accountService, toastService, ); } diff --git a/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover.component.ts b/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover.component.ts index d683545db59..ede60887725 100644 --- a/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover.component.ts @@ -71,15 +71,15 @@ export class EmergencyAccessTakeoverComponent protected toastService: ToastService, ) { super( + accountService, + dialogService, i18nService, + kdfConfigService, keyService, + masterPasswordService, messagingService, platformUtilsService, policyService, - dialogService, - kdfConfigService, - masterPasswordService, - accountService, toastService, ); } diff --git a/apps/web/src/app/auth/update-password.component.ts b/apps/web/src/app/auth/update-password.component.ts index c975f7c4168..4c48d1fef89 100644 --- a/apps/web/src/app/auth/update-password.component.ts +++ b/apps/web/src/app/auth/update-password.component.ts @@ -1,11 +1,10 @@ import { Component, inject } from "@angular/core"; import { UpdatePasswordComponent as BaseUpdatePasswordComponent } from "@bitwarden/angular/auth/components/update-password.component"; +import { OrganizationInviteService } from "@bitwarden/common/auth/services/organization-invite/organization-invite-service"; import { RouterService } from "../core"; -import { AcceptOrganizationInviteService } from "./organization-invite/accept-organization.service"; - @Component({ selector: "app-update-password", templateUrl: "update-password.component.html", @@ -13,13 +12,13 @@ import { AcceptOrganizationInviteService } from "./organization-invite/accept-or }) export class UpdatePasswordComponent extends BaseUpdatePasswordComponent { private routerService = inject(RouterService); - private acceptOrganizationInviteService = inject(AcceptOrganizationInviteService); + private organizationInviteService = inject(OrganizationInviteService); 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 this.organizationInviteService.clearOrganizationInvitation(); await super.cancel(); } } diff --git a/apps/web/src/app/billing/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.ts b/apps/web/src/app/billing/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.ts index 215035a0d16..a7fd8634c23 100644 --- a/apps/web/src/app/billing/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.ts +++ b/apps/web/src/app/billing/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.ts @@ -18,6 +18,7 @@ import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/mod import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { OrganizationInviteService } from "@bitwarden/common/auth/services/organization-invite/organization-invite-service"; import { OrganizationBillingServiceAbstraction as OrganizationBillingService, OrganizationInformation, @@ -31,7 +32,6 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { ToastService } from "@bitwarden/components"; -import { AcceptOrganizationInviteService } from "../../../auth/organization-invite/accept-organization.service"; import { OrganizationCreatedEvent, SubscriptionProduct, @@ -112,7 +112,7 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy { private i18nService: I18nService, private routerService: RouterService, private organizationBillingService: OrganizationBillingService, - private acceptOrganizationInviteService: AcceptOrganizationInviteService, + private organizationInviteService: OrganizationInviteService, private toastService: ToastService, private registrationFinishService: RegistrationFinishService, private validationService: ValidationService, @@ -171,7 +171,7 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy { this.setupFamilySponsorship(qParams.sponsorshipToken); }); - const invite = await this.acceptOrganizationInviteService.getOrganizationInvite(); + const invite = await this.organizationInviteService.getOrganizationInvite(); let policies: Policy[] | null = null; if (invite != null) { diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index ea54b1972a9..e6fe62b74c0 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -51,6 +51,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; +import { OrganizationInviteService } from "@bitwarden/common/auth/services/organization-invite/organization-invite-service"; import { ClientType } from "@bitwarden/common/enums"; import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service"; import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service"; @@ -120,7 +121,6 @@ import { LinkSsoService, } from "../auth"; import { WebSsoComponentService } from "../auth/core/services/login/web-sso-component.service"; -import { AcceptOrganizationInviteService } from "../auth/organization-invite/accept-organization.service"; import { HtmlStorageService } from "../core/html-storage.service"; import { I18nService } from "../core/i18n.service"; import { WebFileDownloadService } from "../core/web-file-download.service"; @@ -250,11 +250,10 @@ const safeProviders: SafeProvider[] = [ deps: [ KeyServiceAbstraction, AccountApiServiceAbstraction, - AcceptOrganizationInviteService, + OrganizationInviteService, PolicyApiServiceAbstraction, LogService, PolicyService, - AccountService, ], }), safeProvider({ @@ -272,16 +271,16 @@ const safeProviders: SafeProvider[] = [ provide: SetPasswordJitService, useClass: WebSetPasswordJitService, deps: [ - ApiService, - MasterPasswordApiService, - KeyServiceAbstraction, EncryptService, I18nServiceAbstraction, KdfConfigService, + KeyServiceAbstraction, + MasterPasswordApiService, InternalMasterPasswordServiceAbstraction, OrganizationApiServiceAbstraction, OrganizationUserApiService, InternalUserDecryptionOptionsServiceAbstraction, + OrganizationInviteService, ], }), safeProvider({ @@ -293,7 +292,7 @@ const safeProviders: SafeProvider[] = [ provide: LoginComponentService, useClass: WebLoginComponentService, deps: [ - AcceptOrganizationInviteService, + OrganizationInviteService, LogService, PolicyApiServiceAbstraction, InternalPolicyService, @@ -358,7 +357,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: LoginDecryptionOptionsService, useClass: WebLoginDecryptionOptionsService, - deps: [MessagingService, RouterService, AcceptOrganizationInviteService], + deps: [MessagingService, RouterService, OrganizationInviteService], }), safeProvider({ provide: IpcService, diff --git a/libs/angular/src/auth/components/change-password.component.ts b/libs/angular/src/auth/components/change-password.component.ts index ca81f741b23..6adb684681c 100644 --- a/libs/angular/src/auth/components/change-password.component.ts +++ b/libs/angular/src/auth/components/change-password.component.ts @@ -37,15 +37,15 @@ export class ChangePasswordComponent implements OnInit, OnDestroy { protected destroy$ = new Subject(); constructor( + protected accountService: AccountService, + protected dialogService: DialogService, protected i18nService: I18nService, + protected kdfConfigService: KdfConfigService, protected keyService: KeyService, + protected masterPasswordService: InternalMasterPasswordServiceAbstraction, protected messagingService: MessagingService, protected platformUtilsService: PlatformUtilsService, protected policyService: PolicyService, - protected dialogService: DialogService, - protected kdfConfigService: KdfConfigService, - protected masterPasswordService: InternalMasterPasswordServiceAbstraction, - protected accountService: AccountService, protected toastService: ToastService, ) {} diff --git a/libs/angular/src/auth/components/set-password.component.ts b/libs/angular/src/auth/components/set-password.component.ts index aadc99bdd9b..a26d911797d 100644 --- a/libs/angular/src/auth/components/set-password.component.ts +++ b/libs/angular/src/auth/components/set-password.component.ts @@ -14,7 +14,6 @@ import { // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; 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"; @@ -58,38 +57,37 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements ForceSetPasswordReason = ForceSetPasswordReason; constructor( - accountService: AccountService, - masterPasswordService: InternalMasterPasswordServiceAbstraction, - i18nService: I18nService, - keyService: KeyService, - messagingService: MessagingService, - platformUtilsService: PlatformUtilsService, - private policyApiService: PolicyApiServiceAbstraction, - policyService: PolicyService, + protected accountService: AccountService, + protected dialogService: DialogService, + protected i18nService: I18nService, + protected kdfConfigService: KdfConfigService, + protected keyService: KeyService, + protected masterPasswordService: InternalMasterPasswordServiceAbstraction, + protected messagingService: MessagingService, + protected platformUtilsService: PlatformUtilsService, + protected policyService: PolicyService, protected router: Router, + protected toastService: ToastService, + private encryptService: EncryptService, private masterPasswordApiService: MasterPasswordApiService, - private apiService: ApiService, - private syncService: SyncService, - private route: ActivatedRoute, private organizationApiService: OrganizationApiServiceAbstraction, private organizationUserApiService: OrganizationUserApiService, - private userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, + private policyApiService: PolicyApiServiceAbstraction, + private activatedRoute: ActivatedRoute, private ssoLoginService: SsoLoginServiceAbstraction, - dialogService: DialogService, - kdfConfigService: KdfConfigService, - private encryptService: EncryptService, - protected toastService: ToastService, + private syncService: SyncService, + private userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, ) { super( + accountService, + dialogService, i18nService, + kdfConfigService, keyService, + masterPasswordService, messagingService, platformUtilsService, policyService, - dialogService, - kdfConfigService, - masterPasswordService, - accountService, toastService, ); } @@ -108,7 +106,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements this.masterPasswordService.forceSetPasswordReason$(this.activeUserId), ); - this.route.queryParams + this.activatedRoute.queryParams .pipe( first(), switchMap((qParams) => { diff --git a/libs/angular/src/auth/components/update-password.component.ts b/libs/angular/src/auth/components/update-password.component.ts index 47affbecdf2..839c3b24ebf 100644 --- a/libs/angular/src/auth/components/update-password.component.ts +++ b/libs/angular/src/auth/components/update-password.component.ts @@ -52,15 +52,15 @@ export class UpdatePasswordComponent extends BaseChangePasswordComponent { toastService: ToastService, ) { super( + accountService, + dialogService, i18nService, + kdfConfigService, keyService, + masterPasswordService, messagingService, platformUtilsService, policyService, - dialogService, - kdfConfigService, - masterPasswordService, - accountService, toastService, ); } diff --git a/libs/angular/src/auth/components/update-temp-password.component.ts b/libs/angular/src/auth/components/update-temp-password.component.ts index db2f319998a..87db26a6b59 100644 --- a/libs/angular/src/auth/components/update-temp-password.component.ts +++ b/libs/angular/src/auth/components/update-temp-password.component.ts @@ -64,15 +64,15 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent imp toastService: ToastService, ) { super( + accountService, + dialogService, i18nService, + kdfConfigService, keyService, + masterPasswordService, messagingService, platformUtilsService, policyService, - dialogService, - kdfConfigService, - masterPasswordService, - accountService, toastService, ); } diff --git a/libs/angular/src/auth/guards/auth.guard.ts b/libs/angular/src/auth/guards/auth.guard.ts index a339200be1d..36baf8499fe 100644 --- a/libs/angular/src/auth/guards/auth.guard.ts +++ b/libs/angular/src/auth/guards/auth.guard.ts @@ -68,20 +68,27 @@ export const authGuard: CanActivateFn = async ( return router.createUrlTree(["/set-password"]); } - // When the PM16117_ChangeExistingPasswordRefactor flag is removed also remove the conditional check - // for update-temp-password here. That route will no longer be in effect. - if ( - forceSetPasswordReason !== ForceSetPasswordReason.None && - !routerState.url.includes("update-temp-password") && - !routerState.url.includes("change-password") - ) { - const setInitialPasswordRefactorFlagOn = await configService.getFeatureFlag( - FeatureFlag.PM16117_ChangeExistingPasswordRefactor, - ); + if (await configService.getFeatureFlag(FeatureFlag.PM16117_ChangeExistingPasswordRefactor)) { + // When the PM16117_ChangeExistingPasswordRefactor flag is removed AS WELL AS the cleanup for + // update-temp-password also remove the conditional check for update-temp-password here. + // That route will no longer be in effect. + if ( + (forceSetPasswordReason === ForceSetPasswordReason.AdminForcePasswordReset || + forceSetPasswordReason === ForceSetPasswordReason.WeakMasterPassword) && + !routerState.url.includes("update-temp-password") && + !routerState.url.includes("change-password") + ) { + return router.createUrlTree(["/change-password"]); + } - const route = setInitialPasswordRefactorFlagOn ? "/change-password" : "/update-temp-password"; - - return router.createUrlTree([route]); + // Remove this else condition when taking out the PM16117_ChangeExistingPasswordRefactor flag. + } else { + if ( + forceSetPasswordReason !== ForceSetPasswordReason.None && + !routerState.url.includes("update-temp-password") + ) { + return router.createUrlTree(["/update-temp-password"]); + } } return true; diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 84a30c69947..9b665355b95 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -1390,12 +1390,11 @@ const safeProviders: SafeProvider[] = [ provide: SetPasswordJitService, useClass: DefaultSetPasswordJitService, deps: [ - ApiServiceAbstraction, - MasterPasswordApiServiceAbstraction, - KeyService, EncryptService, I18nServiceAbstraction, KdfConfigService, + KeyService, + MasterPasswordApiServiceAbstraction, InternalMasterPasswordServiceAbstraction, OrganizationApiServiceAbstraction, OrganizationUserApiService, diff --git a/libs/auth/src/angular/change-password/change-password.component.ts b/libs/auth/src/angular/change-password/change-password.component.ts index 26a6118e1d6..2cd9fc014fa 100644 --- a/libs/auth/src/angular/change-password/change-password.component.ts +++ b/libs/auth/src/angular/change-password/change-password.component.ts @@ -1,7 +1,7 @@ import { Component, Input, OnInit } from "@angular/core"; import { firstValueFrom } from "rxjs"; -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 { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; @@ -60,7 +60,7 @@ export class ChangePasswordComponent implements OnInit { private masterPasswordService: InternalMasterPasswordServiceAbstraction, private anonLayoutWrapperDataService: AnonLayoutWrapperDataService, private messagingService: MessagingService, - private policyApiService: PolicyApiServiceAbstraction, + private policyService: PolicyService, private toastService: ToastService, private syncService: SyncService, private dialogService: DialogService, @@ -81,9 +81,10 @@ export class ChangePasswordComponent implements OnInit { throw new Error("userId not found"); } - this.masterPasswordPolicyOptions = MasterPasswordPolicyOptions.fromResponse( - await this.policyApiService.getMasterPasswordPoliciesForAcceptedOrConfirmedUser(), - ); + // New Master Password Policy Options service + // this.masterPasswordPolicyOptions = await firstValueFrom( + // this.policyService.postAuthenticatedMasterPasswordPolicyOptions$(this.userId) + // ) ?? undefined; this.forceSetPasswordReason = await firstValueFrom( this.masterPasswordService.forceSetPasswordReason$(this.userId), diff --git a/libs/auth/src/angular/login/login.component.ts b/libs/auth/src/angular/login/login.component.ts index 4526fc4d95c..c7de756ee73 100644 --- a/libs/auth/src/angular/login/login.component.ts +++ b/libs/auth/src/angular/login/login.component.ts @@ -17,8 +17,10 @@ import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/mod import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; +import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { ClientType, HttpStatusCode } from "@bitwarden/common/enums"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; @@ -124,6 +126,7 @@ export class LoginComponent implements OnInit, OnDestroy { private logService: LogService, private validationService: ValidationService, private loginSuccessHandlerService: LoginSuccessHandlerService, + private masterPasswordService: MasterPasswordServiceAbstraction, private configService: ConfigService, ) { this.clientType = this.platformUtilsService.getClientType(); @@ -333,8 +336,17 @@ export class LoginComponent implements OnInit, OnDestroy { // The AuthGuard will handle routing to update-temp-password based on state if ( - !(await this.configService.getFeatureFlag(FeatureFlag.PM16117_ChangeExistingPasswordRefactor)) + await this.configService.getFeatureFlag(FeatureFlag.PM16117_ChangeExistingPasswordRefactor) ) { + const forceSetPasswordReason: ForceSetPasswordReason = await firstValueFrom( + this.masterPasswordService.forceSetPasswordReason$(authResult.userId), + ); + + if (forceSetPasswordReason === ForceSetPasswordReason.WeakMasterPassword) { + await this.router.navigate(["change-password"]); + return; + } + } else { // TODO: PM-18269 - evaluate if we can combine this with the // password evaluation done in the password login strategy. // If there's an existing org invite, use it to get the org's password policies diff --git a/libs/auth/src/angular/new-device-verification/new-device-verification.component.ts b/libs/auth/src/angular/new-device-verification/new-device-verification.component.ts index a8aa3bd5525..bf9bc5827b4 100644 --- a/libs/auth/src/angular/new-device-verification/new-device-verification.component.ts +++ b/libs/auth/src/angular/new-device-verification/new-device-verification.component.ts @@ -2,11 +2,17 @@ import { CommonModule } from "@angular/common"; import { Component, OnDestroy, OnInit } from "@angular/core"; import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms"; import { Router } from "@angular/router"; -import { Subject, takeUntil } from "rxjs"; +import { firstValueFrom, Subject, takeUntil } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { LoginSuccessHandlerService } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. @@ -61,6 +67,9 @@ export class NewDeviceVerificationComponent implements OnInit, OnDestroy { private logService: LogService, private i18nService: I18nService, private loginSuccessHandlerService: LoginSuccessHandlerService, + private configService: ConfigService, + private accountService: AccountService, + private masterPasswordService: MasterPasswordServiceAbstraction, ) {} async ngOnInit() { @@ -141,8 +150,27 @@ export class NewDeviceVerificationComponent implements OnInit, OnDestroy { // eslint-disable-next-line @typescript-eslint/no-floating-promises this.loginSuccessHandlerService.run(authResult.userId); - // If verification succeeds, navigate to vault - await this.router.navigate(["/vault"]); + // TODO: PM-22663 use the new service to handle routing. + if ( + await this.configService.getFeatureFlag(FeatureFlag.PM16117_ChangeExistingPasswordRefactor) + ) { + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(getUserId), + ); + + const forceSetPasswordReason = await firstValueFrom( + this.masterPasswordService.forceSetPasswordReason$(activeUserId), + ); + + if ( + forceSetPasswordReason === ForceSetPasswordReason.WeakMasterPassword || + forceSetPasswordReason === ForceSetPasswordReason.AdminForcePasswordReset + ) { + await this.router.navigate(["/change-password"]); + } + } else { + await this.router.navigate(["/vault"]); + } } catch (e) { this.logService.error(e); let errorMessage = diff --git a/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.ts b/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.ts index ec274b9c4af..7d228fccb9b 100644 --- a/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.ts +++ b/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.ts @@ -9,7 +9,6 @@ import { OrganizationUserResetPasswordEnrollmentRequest, } from "@bitwarden/admin-console/common"; import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; @@ -31,12 +30,11 @@ import { export class DefaultSetPasswordJitService implements SetPasswordJitService { constructor( - protected apiService: ApiService, - protected masterPasswordApiService: MasterPasswordApiService, - protected keyService: KeyService, protected encryptService: EncryptService, protected i18nService: I18nService, protected kdfConfigService: KdfConfigService, + protected keyService: KeyService, + protected masterPasswordApiService: MasterPasswordApiService, protected masterPasswordService: InternalMasterPasswordServiceAbstraction, protected organizationApiService: OrganizationApiServiceAbstraction, protected organizationUserApiService: OrganizationUserApiService, diff --git a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts index 315f8121cce..006af96299a 100644 --- a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts +++ b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts @@ -32,7 +32,10 @@ import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-p import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -169,6 +172,7 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy { private loginSuccessHandlerService: LoginSuccessHandlerService, private twoFactorAuthComponentCacheService: TwoFactorAuthComponentCacheService, private authService: AuthService, + private configService: ConfigService, ) {} async ngOnInit() { @@ -506,6 +510,24 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy { return "lock"; } + // TODO: PM-22663 use the new service to handle routing. + if ( + await this.configService.getFeatureFlag(FeatureFlag.PM16117_ChangeExistingPasswordRefactor) + ) { + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + + const forceSetPasswordReason = await firstValueFrom( + this.masterPasswordService.forceSetPasswordReason$(activeUserId), + ); + + if ( + forceSetPasswordReason === ForceSetPasswordReason.WeakMasterPassword || + forceSetPasswordReason === ForceSetPasswordReason.AdminForcePasswordReset + ) { + return "change-password"; + } + } + return "vault"; } diff --git a/libs/auth/src/common/login-strategies/password-login.strategy.ts b/libs/auth/src/common/login-strategies/password-login.strategy.ts index 8734970ba70..0494476e6ad 100644 --- a/libs/auth/src/common/login-strategies/password-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/password-login.strategy.ts @@ -163,7 +163,8 @@ export class PasswordLoginStrategy extends LoginStrategy { credentials: PasswordLoginCredentials, authResult: AuthResult, ): Promise { - // TODO: PM-21084 - investigate if we should be sending down masterPasswordPolicy on the IdentityDeviceVerificationResponse like we do for the IdentityTwoFactorResponse + // TODO: PM-21084 - investigate if we should be sending down masterPasswordPolicy on the + // IdentityDeviceVerificationResponse like we do for the IdentityTwoFactorResponse // If the response is a device verification response, we don't need to evaluate the password if (identityResponse instanceof IdentityDeviceVerificationResponse) { return; @@ -175,11 +176,11 @@ export class PasswordLoginStrategy extends LoginStrategy { if ( await this.configService.getFeatureFlag(FeatureFlag.PM16117_ChangeExistingPasswordRefactor) ) { - // Either take credentials from a potential org invite first, then take from - // the identity response if that doesn't exist. - masterPasswordPolicyOptions = credentials.masterPasswordPoliciesFromOrgInvite - ? credentials.masterPasswordPoliciesFromOrgInvite - : this.getMasterPasswordPolicyOptionsFromResponse(identityResponse); + // Get the master password policy options from both the org invite and the identity response + masterPasswordPolicyOptions = this.policyService.combineMasterPasswordPolicyOptions( + credentials.masterPasswordPoliciesFromOrgInvite, + this.getMasterPasswordPolicyOptionsFromResponse(identityResponse), + ); if (!masterPasswordPolicyOptions?.enforceOnLogin) { return; @@ -208,6 +209,8 @@ export class PasswordLoginStrategy extends LoginStrategy { return; } + // Also set master password policy options here + // Authentication was successful, save the force update password options with the state service // if there isn't already a reason set (this would only be AdminForcePasswordReset as that can be set server side // and would have already been processed in the base login strategy processForceSetPasswordReason method) diff --git a/libs/common/src/admin-console/abstractions/policy/policy.service.abstraction.ts b/libs/common/src/admin-console/abstractions/policy/policy.service.abstraction.ts index 6707af460b6..8df7e44986b 100644 --- a/libs/common/src/admin-console/abstractions/policy/policy.service.abstraction.ts +++ b/libs/common/src/admin-console/abstractions/policy/policy.service.abstraction.ts @@ -51,13 +51,24 @@ export abstract class PolicyService { ) => Observable; /** - * Combines all Master Password policies that are passed in. + * Combines all Master Password policies that are passed in and returns + * back the strongest combination of all the policies in the form of a + * MasterPasswordPolicyOptions. * @param policies */ - abstract combineMasterPasswordPolicies( + abstract combinePoliciesIntoMasterPasswordPolicyOptions( policies: Policy[], ): MasterPasswordPolicyOptions | undefined; + /** + * Takes an arbitrary amount of Master Password Policy options in any form and merges them + * together using the strictest combination of all of them. + * @param masterPasswordPolicyOptions + */ + abstract combineMasterPasswordPolicyOptions( + ...masterPasswordPolicyOptions: MasterPasswordPolicyOptions[] + ): MasterPasswordPolicyOptions | undefined; + /** * Evaluates whether a proposed Master Password complies with all Master Password policies that apply to the user. */ diff --git a/libs/common/src/admin-console/models/domain/master-password-policy-options.ts b/libs/common/src/admin-console/models/domain/master-password-policy-options.ts index 8447f8cf8e0..340b21aaf0d 100644 --- a/libs/common/src/admin-console/models/domain/master-password-policy-options.ts +++ b/libs/common/src/admin-console/models/domain/master-password-policy-options.ts @@ -19,16 +19,7 @@ export class MasterPasswordPolicyOptions extends Domain { enforceOnLogin = false; static fromResponse(policy: MasterPasswordPolicyResponse): MasterPasswordPolicyOptions { - // Check if the policy is null or if all the values in the response object is null. - // Exclude the response object because the MasterPasswordPolicyResponse extends - // BaseResponse and we should omit that when checking for null values. Doing this - // programmatically makes this less brittle for future contract changes. - if ( - policy == null || - Object.entries(policy) - .filter(([key]) => key !== "response") - .every(([, value]) => value == null) - ) { + if (policy == null) { return null; } const options = new MasterPasswordPolicyOptions(); diff --git a/libs/common/src/admin-console/services/policy/default-policy.service.ts b/libs/common/src/admin-console/services/policy/default-policy.service.ts index 6abb7f36528..e00de0a74fc 100644 --- a/libs/common/src/admin-console/services/policy/default-policy.service.ts +++ b/libs/common/src/admin-console/services/policy/default-policy.service.ts @@ -87,10 +87,14 @@ export class DefaultPolicyService implements PolicyService { policies?: Policy[], ): Observable { const policies$ = policies ? of(policies) : this.policies$(userId); - return policies$.pipe(map((obsPolicies) => this.combineMasterPasswordPolicies(obsPolicies))); + return policies$.pipe( + map((obsPolicies) => this.combinePoliciesIntoMasterPasswordPolicyOptions(obsPolicies)), + ); } - combineMasterPasswordPolicies(policies: Policy[]): MasterPasswordPolicyOptions | undefined { + combinePoliciesIntoMasterPasswordPolicyOptions( + policies: Policy[], + ): MasterPasswordPolicyOptions | undefined { let enforcedOptions: MasterPasswordPolicyOptions | undefined = undefined; const filteredPolicies = policies.filter((p) => p.type === PolicyType.MasterPassword) ?? []; @@ -100,51 +104,35 @@ export class DefaultPolicyService implements PolicyService { filteredPolicies.forEach((currentPolicy) => { if (!currentPolicy.enabled || !currentPolicy.data) { - return; + return undefined; } if (!enforcedOptions) { enforcedOptions = new MasterPasswordPolicyOptions(); } - if ( - currentPolicy.data.minComplexity != null && - currentPolicy.data.minComplexity > enforcedOptions.minComplexity - ) { - enforcedOptions.minComplexity = currentPolicy.data.minComplexity; - } - - if ( - currentPolicy.data.minLength != null && - currentPolicy.data.minLength > enforcedOptions.minLength - ) { - enforcedOptions.minLength = currentPolicy.data.minLength; - } - - if (currentPolicy.data.requireUpper) { - enforcedOptions.requireUpper = true; - } - - if (currentPolicy.data.requireLower) { - enforcedOptions.requireLower = true; - } - - if (currentPolicy.data.requireNumbers) { - enforcedOptions.requireNumbers = true; - } - - if (currentPolicy.data.requireSpecial) { - enforcedOptions.requireSpecial = true; - } - - if (currentPolicy.data.enforceOnLogin) { - enforcedOptions.enforceOnLogin = true; - } + this.mergeMasterPasswordPolicyOptions(enforcedOptions, currentPolicy.data); }); return enforcedOptions; } + combineMasterPasswordPolicyOptions( + ...policies: MasterPasswordPolicyOptions[] + ): MasterPasswordPolicyOptions | undefined { + let combinedOptions: MasterPasswordPolicyOptions | undefined = undefined; + + policies.forEach((currentOptions) => { + if (!combinedOptions) { + combinedOptions = new MasterPasswordPolicyOptions(); + } + + this.mergeMasterPasswordPolicyOptions(combinedOptions, currentOptions); + }); + + return combinedOptions; + } + evaluateMasterPassword( passwordStrength: number, newPassword: string, @@ -240,4 +228,26 @@ export class DefaultPolicyService implements PolicyService { return organization.canManagePolicies; } } + + private mergeMasterPasswordPolicyOptions( + target: MasterPasswordPolicyOptions | undefined, + source: MasterPasswordPolicyOptions | undefined, + ) { + if (!target) { + target = new MasterPasswordPolicyOptions(); + } + + if (source) { + target.minComplexity = Math.max( + target.minComplexity, + source.minComplexity ?? target.minComplexity, + ); + target.minLength = Math.max(target.minLength, source.minLength ?? target.minLength); + target.requireUpper = target.requireUpper || source.requireUpper; + target.requireLower = target.requireLower || source.requireLower; + target.requireNumbers = target.requireNumbers || source.requireNumbers; + target.requireSpecial = target.requireSpecial || source.requireSpecial; + target.enforceOnLogin = target.enforceOnLogin || source.enforceOnLogin; + } + } } diff --git a/libs/common/src/auth/services/organization-invite/organization-invite-service.ts b/libs/common/src/auth/services/organization-invite/organization-invite-service.ts new file mode 100644 index 00000000000..d0df233e7aa --- /dev/null +++ b/libs/common/src/auth/services/organization-invite/organization-invite-service.ts @@ -0,0 +1,37 @@ +import { firstValueFrom } from "rxjs"; + +import { OrganizationInvite } from "@bitwarden/common/auth/services/organization-invite/organization-invite"; +import { ORGANIZATION_INVITE } from "@bitwarden/common/auth/services/organization-invite/organization-invite-state"; +import { GlobalState, GlobalStateProvider } from "@bitwarden/common/platform/state"; + +export class OrganizationInviteService { + private organizationInvitationState: GlobalState; + + constructor(private readonly globalStateProvider: GlobalStateProvider) { + this.organizationInvitationState = this.globalStateProvider.get(ORGANIZATION_INVITE); + } + + /** + * Returns the currently stored organization invite + */ + async getOrganizationInvite(): Promise { + 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 { + 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 { + await this.organizationInvitationState.update(() => null); + } +} diff --git a/libs/common/src/auth/services/organization-invite/organization-invite-state.ts b/libs/common/src/auth/services/organization-invite/organization-invite-state.ts new file mode 100644 index 00000000000..c544fa3269f --- /dev/null +++ b/libs/common/src/auth/services/organization-invite/organization-invite-state.ts @@ -0,0 +1,13 @@ +import { OrganizationInvite } from "@bitwarden/common/auth/services/organization-invite/organization-invite"; +import { KeyDefinition, ORGANIZATION_INVITE_DISK } from "@bitwarden/common/platform/state"; + +// 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( + ORGANIZATION_INVITE_DISK, + "organizationInvite", + { + deserializer: (invite) => (invite ? OrganizationInvite.fromJSON(invite) : null), + }, +); diff --git a/libs/common/src/auth/services/organization-invite/organization-invite.ts b/libs/common/src/auth/services/organization-invite/organization-invite.ts new file mode 100644 index 00000000000..d18fdcedb41 --- /dev/null +++ b/libs/common/src/auth/services/organization-invite/organization-invite.ts @@ -0,0 +1,20 @@ +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 | null { + if (json == null) { + return null; + } + + return Object.assign(new OrganizationInvite(), json); + } +}