1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-06 19:53:59 +00:00

fix(change-password-component): Change Password Update [18720] - Took org invite state out of service and made it accessible.

This commit is contained in:
Patrick Pimentel
2025-06-22 20:31:20 -04:00
parent 815f379c24
commit 735a114baa
33 changed files with 417 additions and 317 deletions

View File

@@ -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);
}

View File

@@ -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<PasswordPolicies | null> {
const orgInvite = await this.acceptOrganizationInviteService.getOrganizationInvite();
async getOrgPoliciesFromOrgInvite(): Promise<PasswordPolicies | undefined> {
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(

View File

@@ -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<KeyService>;
let accountApiService: MockProxy<AccountApiService>;
let acceptOrgInviteService: MockProxy<AcceptOrganizationInviteService>;
let organizationInviteService: MockProxy<OrganizationInviteService>;
let policyApiService: MockProxy<PolicyApiServiceAbstraction>;
let logService: MockProxy<LogService>;
let policyService: MockProxy<PolicyService>;
const mockUserId = Utils.newGuid() as UserId;
let accountService: FakeAccountService;
beforeEach(() => {
keyService = mock<KeyService>();
accountApiService = mock<AccountApiService>();
acceptOrgInviteService = mock<AcceptOrganizationInviteService>();
organizationInviteService = mock<OrganizationInviteService>();
policyApiService = mock<PolicyApiServiceAbstraction>();
logService = mock<LogService>();
policyService = mock<PolicyService>();
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,

View File

@@ -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<string | null> {
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<MasterPasswordPolicyOptions | null> {
// 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;

View File

@@ -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();
}
}

View File

@@ -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<void> {
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<void> {
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,
});
}
}

View File

@@ -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<LogService>;
let organizationApiService: MockProxy<OrganizationApiServiceAbstraction>;
let organizationUserApiService: MockProxy<OrganizationUserApiService>;
let organizationInviteService: MockProxy<OrganizationInviteService>;
let i18nService: MockProxy<I18nService>;
let globalStateProvider: FakeGlobalStateProvider;
let globalState: FakeGlobalState<OrganizationInvite>;
let dialogService: MockProxy<DialogService>;
let accountService: MockProxy<AccountService>;
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,
);
});

View File

@@ -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<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[];
@@ -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<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.
@@ -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

View File

@@ -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>): 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,
});
}
}

View File

@@ -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();
}
}

View File

@@ -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,
);
}

View File

@@ -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,
);
}

View File

@@ -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();
}
}

View File

@@ -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) {

View File

@@ -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,

View File

@@ -37,15 +37,15 @@ export class ChangePasswordComponent implements OnInit, OnDestroy {
protected destroy$ = new Subject<void>();
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,
) {}

View File

@@ -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) => {

View File

@@ -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,
);
}

View File

@@ -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,
);
}

View File

@@ -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;

View File

@@ -1390,12 +1390,11 @@ const safeProviders: SafeProvider[] = [
provide: SetPasswordJitService,
useClass: DefaultSetPasswordJitService,
deps: [
ApiServiceAbstraction,
MasterPasswordApiServiceAbstraction,
KeyService,
EncryptService,
I18nServiceAbstraction,
KdfConfigService,
KeyService,
MasterPasswordApiServiceAbstraction,
InternalMasterPasswordServiceAbstraction,
OrganizationApiServiceAbstraction,
OrganizationUserApiService,

View File

@@ -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),

View File

@@ -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

View File

@@ -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 =

View File

@@ -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,

View File

@@ -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";
}

View File

@@ -163,7 +163,8 @@ export class PasswordLoginStrategy extends LoginStrategy {
credentials: PasswordLoginCredentials,
authResult: AuthResult,
): Promise<void> {
// 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)

View File

@@ -51,13 +51,24 @@ export abstract class PolicyService {
) => Observable<MasterPasswordPolicyOptions | undefined>;
/**
* 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.
*/

View File

@@ -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();

View File

@@ -87,10 +87,14 @@ export class DefaultPolicyService implements PolicyService {
policies?: Policy[],
): Observable<MasterPasswordPolicyOptions | undefined> {
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;
}
}
}

View File

@@ -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<OrganizationInvite | null>;
constructor(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);
}
}

View File

@@ -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<OrganizationInvite | null>(
ORGANIZATION_INVITE_DISK,
"organizationInvite",
{
deserializer: (invite) => (invite ? OrganizationInvite.fromJSON(invite) : null),
},
);

View File

@@ -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>): OrganizationInvite | null {
if (json == null) {
return null;
}
return Object.assign(new OrganizationInvite(), json);
}
}