1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-25 20:53:22 +00:00

feat(change-password): [PM-18720] (#5319) Change Password Implementation for Non Dialog Cases (#15319)

* feat(change-password-component): Change Password Update [18720] - Very close to complete.

* fix(policy-enforcement): [PM-21085] Fix Bug with Policy Enforcement - Removed temp code to force the state I need to verify correctness.

* fix(policy-enforcement): [PM-21085] Fix Bug with Policy Enforcement - Recover account working with change password component.

* fix(policy-enforcement): [PM-21085] Fix Bug with Policy Enforcement - Made code more dry.

* fix(change-password-component): Change Password Update [18720] - Updates to routing and the extension. Extension is still a wip.

* fix(change-password-component): Change Password Update [18720] - Extension routing changes.

* feat(change-password-component): Change Password Update [18720] - More extension work

* feat(change-password-component): Change Password Update [18720] - Pausing work for now while we wait for product to hear back.

* feat(change-password-component): Change Password Update [18720] - Removed duplicated anon layouts.

* feat(change-password-component): Change Password Update [18720] - Tidied up code.

* feat(change-password-component): Change Password Update [18720] - Small fixes to the styling

* feat(change-password-component): Change Password Update [18720] - Adding more content for the routing.

* feat(change-password-component): Change Password Update [18720] - Removed circular loop for now.

* feat(change-password-component): Change Password Update [18720] - Made comments regarding the change password routing complexities with change-password and auth guard.

* feat(change-password-component): Change Password Update [18720] - Undid some changes because they will be conflicts later on.

* feat(change-password-component): Change Password Update [18720] - Small directive change.

* feat(change-password-component): Change Password Update [18720] - Small changes and added some clarification on where I'm blocked

* feat(change-password-component): Change Password Update [18720] - Org invite is seemingly working, found one bug to iron out.

* refactor(change-password-component): Change Password Update [18720] - Fixed up policy service to be made more clear.

* docs(change-password-component): Change Password Update [18720] - Updated documentation.

* refactor(change-password-component): Change Password Update [18720] - Routing changes and policy service changes.

* fix(change-password-component): Change Password Update [18720] - Wrapping up changes.

* feat(change-password-component): Change Password Update [18720] - Should be working fully

* feat(change-password-component): Change Password Update [18720] - Found a bug, working on password policy being present on login.

* feat(change-password-component): Change Password Update [18720] - Turned on auth guard on other clients for change-password route.

* feat(change-password-component): Change Password Update [18720] - Committing intermediate changes.

* feat(change-password-component): Change Password Update [18720] - The master password policy endpoint has been added! Should be working. Testing now.

* feat(change-password-component): Change Password Update [18720] - Minor fixes.

* feat(change-password-component): Change Password Update [18720] - Undid naming change.

* feat(change-password-component): Change Password Update [18720] - Removed comment.

* feat(change-password-component): Change Password Update [18720] - Removed unneeded code.

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

* fix(change-password-component): Change Password Update [18720] - Small changes.

* fix(change-password-component): Change Password Update [18720] - Split up org invite service into client specific implementations and have them injected into clients properly

* feat(change-password-component): Change Password Update [18720] - Stopping work and going to switch to a new branch to pare down some of the solutions that were made to get this over the finish line

* feat(change-password-component): Change Password Update [18720] - Started to remove functionality in the login.component and the password login strategy.

* feat(change-password-component): Change Password Update [18720] - Removed more unneded changes.

* feat(change-password-component): Change Password Update [18720] - Change password clearing state working properly.

* fix(change-password-component): Change Password Update [18720] - Added docs and moved web implementation.

* comments(change-password-component): Change Password Update [18720] - Added more notes.

* test(change-password-component): Change Password Update [18720] - Added in tests for policy service.

* comment(change-password-component): Change Password Update [18720] - Updated doc with correct ticket number.

* comment(change-password-component): Change Password Update [18720] - Fixed doc.

* test(change-password-component): Change Password Update [18720] - Fixed tests.

* test(change-password-component): Change Password Update [18720] - Fixed linting errors. Have more tests to fix.

* test(change-password-component): Change Password Update [18720] - Added back in ignore for typesafety.

* fix(change-password-component): Change Password Update [18720] - Fixed other type issues.

* test(change-password-component): Change Password Update [18720] - Fixed tests.

* test(change-password-component): Change Password Update [18720] - Fixed more tests.

* test(change-password-component): Change Password Update [18720] - Fixed tiny duplicate code.

* fix(change-password-component): Change Password Update [18720] - Fixed desktop component.

* fix(change-password-component): Change Password Update [18720] - Removed unused code

* fix(change-password-component): Change Password Update [18720] - Fixed locales.

* fix(change-password-component): Change Password Update [18720] - Removed tracing.

* fix(change-password-component): Change Password Update [18720] - Removed duplicative services module entry.

* fix(change-password-component): Change Password Update [18720] - Added comment.

* fix(change-password-component): Change Password Update [18720] - Fixed unneeded call in two factor to get user id.

* fix(change-password-component): Change Password Update [18720] - Fixed a couple of tiny things.

* fix(change-password-component): Change Password Update [18720] - Added comment for later fix.

* fix(change-password-component): Change Password Update [18720] - Fixed linting error.

* PM-18720 - AuthGuard - move call to get isChangePasswordFlagOn down after other conditions for efficiency.

* PM-18720 - PasswordLoginStrategy tests - test new feature flagged combine org invite policies logic for weak password evaluation.

* PM-18720 - CLI - fix dep issue

* PM-18720 - ChangePasswordComp - extract change password warning up out of input password component

* PM-18720 - InputPassword - remove unused dependency.

* PM-18720 - ChangePasswordComp - add callout dep

* PM-18720 - Revert all anon-layout changes

* PM-18720 - Anon Layout - finish reverting changes.

* PM-18720 - WIP move of change password out of libs/auth

* PM-18720 - Clean up remaining imports from moving change password out of libs/auth

* PM-18720 - Add change-password barrel file for better import grouping

* PM-18720 - Change Password comp - restore maxWidth

* PM-18720 - After merge, fix errors

* PM-18720 - Desktop - fix api service import

* PM-18720 - NDV - fix routing.

* PM-18720 - Change Password Comp - add logout service todo

* PM-18720 - PasswordSettings - per feedback, component is already feature flagged behind PM16117_ChangeExistingPasswordRefactor so we can just delete the replaced callout (new text is in change-password comp)

* PM-18720 - Routing Modules - properly flag new component behind feature flag.

* PM-18720 - SSO Login Strategy - fix config service import since it is now in shared deps from main merge.

* PM-18720 - Fix SSO login strategy tests

* PM-18720 - Default Policy Service - address AC PR feedback

---------

Co-authored-by: Jared Snider <jsnider@bitwarden.com>
Co-authored-by: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com>
This commit is contained in:
Patrick-Pimentel-Bitwarden
2025-07-10 09:08:25 -04:00
committed by GitHub
parent ec015bd253
commit 1f60bcdcc0
70 changed files with 1301 additions and 495 deletions

View File

@@ -1,8 +1,12 @@
import { ChangePasswordService, DefaultChangePasswordService } from "@bitwarden/auth/angular";
import {
ChangePasswordService,
DefaultChangePasswordService,
} from "@bitwarden/angular/auth/password-management/change-password";
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { KeyService } from "@bitwarden/key-management";
import { RouterService } from "@bitwarden/web-vault/app/core";
import { UserKeyRotationService } from "@bitwarden/web-vault/app/key-management/key-rotation/user-key-rotation.service";
export class WebChangePasswordService
@@ -14,6 +18,7 @@ export class WebChangePasswordService
protected masterPasswordApiService: MasterPasswordApiService,
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
private userKeyRotationService: UserKeyRotationService,
private routerService: RouterService,
) {
super(keyService, masterPasswordApiService, masterPasswordService);
}
@@ -31,4 +36,8 @@ export class WebChangePasswordService
newPasswordHint,
);
}
async clearDeeplinkState() {
await this.routerService.getAndClearLoginRedirectUrl();
}
}

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

@@ -10,7 +10,9 @@ import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { ResetPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/reset-password-policy-options";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { OrganizationInviteService } from "@bitwarden/common/auth/services/organization-invite/organization-invite.service";
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@@ -22,7 +24,6 @@ import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legac
// FIXME: remove `src` and fix import
// eslint-disable-next-line no-restricted-imports
import { RouterService } from "../../../../../../../../apps/web/src/app/core";
import { AcceptOrganizationInviteService } from "../../../organization-invite/accept-organization.service";
import { WebLoginComponentService } from "./web-login-component.service";
@@ -32,7 +33,7 @@ jest.mock("../../../../../utils/flags", () => ({
describe("WebLoginComponentService", () => {
let service: WebLoginComponentService;
let acceptOrganizationInviteService: MockProxy<AcceptOrganizationInviteService>;
let organizationInviteService: MockProxy<OrganizationInviteService>;
let logService: MockProxy<LogService>;
let policyApiService: MockProxy<PolicyApiServiceAbstraction>;
let internalPolicyService: MockProxy<InternalPolicyService>;
@@ -44,9 +45,10 @@ describe("WebLoginComponentService", () => {
let ssoLoginService: MockProxy<SsoLoginServiceAbstraction>;
const mockUserId = Utils.newGuid() as UserId;
let accountService: FakeAccountService;
let configService: MockProxy<ConfigService>;
beforeEach(() => {
acceptOrganizationInviteService = mock<AcceptOrganizationInviteService>();
organizationInviteService = mock<OrganizationInviteService>();
logService = mock<LogService>();
policyApiService = mock<PolicyApiServiceAbstraction>();
internalPolicyService = mock<InternalPolicyService>();
@@ -57,12 +59,13 @@ describe("WebLoginComponentService", () => {
platformUtilsService = mock<PlatformUtilsService>();
ssoLoginService = mock<SsoLoginServiceAbstraction>();
accountService = mockAccountServiceWith(mockUserId);
configService = mock<ConfigService>();
TestBed.configureTestingModule({
providers: [
WebLoginComponentService,
{ provide: DefaultLoginComponentService, useClass: WebLoginComponentService },
{ provide: AcceptOrganizationInviteService, useValue: acceptOrganizationInviteService },
{ provide: OrganizationInviteService, useValue: organizationInviteService },
{ provide: LogService, useValue: logService },
{ provide: PolicyApiServiceAbstraction, useValue: policyApiService },
{ provide: InternalPolicyService, useValue: internalPolicyService },
@@ -73,6 +76,7 @@ describe("WebLoginComponentService", () => {
{ provide: PlatformUtilsService, useValue: platformUtilsService },
{ provide: SsoLoginServiceAbstraction, useValue: ssoLoginService },
{ provide: AccountService, useValue: accountService },
{ provide: ConfigService, useValue: configService },
],
});
service = TestBed.inject(WebLoginComponentService);
@@ -84,14 +88,14 @@ describe("WebLoginComponentService", () => {
describe("getOrgPoliciesFromOrgInvite", () => {
it("returns undefined if organization invite is null", async () => {
acceptOrganizationInviteService.getOrganizationInvite.mockResolvedValue(null);
organizationInviteService.getOrganizationInvite.mockResolvedValue(null);
const result = await service.getOrgPoliciesFromOrgInvite();
expect(result).toBeUndefined();
});
it("logs an error if getPoliciesByToken throws an error", async () => {
const error = new Error("Test error");
acceptOrganizationInviteService.getOrganizationInvite.mockResolvedValue({
organizationInviteService.getOrganizationInvite.mockResolvedValue({
organizationId: "org-id",
token: "token",
email: "email",
@@ -117,7 +121,7 @@ describe("WebLoginComponentService", () => {
const resetPasswordPolicyOptions = new ResetPasswordPolicyOptions();
resetPasswordPolicyOptions.autoEnrollEnabled = autoEnrollEnabled;
acceptOrganizationInviteService.getOrganizationInvite.mockResolvedValue({
organizationInviteService.getOrganizationInvite.mockResolvedValue({
organizationId: "org-id",
token: "token",
email: "email",

View File

@@ -11,18 +11,21 @@ import {
} from "@bitwarden/auth/angular";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
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";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
import { RouterService } from "../../../../core/router.service";
import { AcceptOrganizationInviteService } from "../../../organization-invite/accept-organization.service";
@Injectable()
export class WebLoginComponentService
@@ -30,7 +33,7 @@ export class WebLoginComponentService
implements LoginComponentService
{
constructor(
protected acceptOrganizationInviteService: AcceptOrganizationInviteService,
protected organizationInviteService: OrganizationInviteService,
protected logService: LogService,
protected policyApiService: PolicyApiServiceAbstraction,
protected policyService: InternalPolicyService,
@@ -42,6 +45,7 @@ export class WebLoginComponentService
ssoLoginService: SsoLoginServiceAbstraction,
private router: Router,
private accountService: AccountService,
private configService: ConfigService,
) {
super(
cryptoFunctionService,
@@ -66,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[];
@@ -84,7 +88,7 @@ export class WebLoginComponentService
}
if (policies == null) {
return;
return undefined;
}
const resetPasswordPolicy = this.policyService.getResetPasswordPolicyOptions(
@@ -95,12 +99,23 @@ export class WebLoginComponentService
const isPolicyAndAutoEnrollEnabled =
resetPasswordPolicy[1] && resetPasswordPolicy[0].autoEnrollEnabled;
const enforcedPasswordPolicyOptions = await firstValueFrom(
this.accountService.activeAccount$.pipe(
getUserId,
switchMap((userId) => this.policyService.masterPasswordPolicyOptions$(userId, policies)),
),
);
let enforcedPasswordPolicyOptions: MasterPasswordPolicyOptions;
if (
await this.configService.getFeatureFlag(FeatureFlag.PM16117_ChangeExistingPasswordRefactor)
) {
enforcedPasswordPolicyOptions =
this.policyService.combinePoliciesIntoMasterPasswordPolicyOptions(policies);
} else {
enforcedPasswordPolicyOptions = await firstValueFrom(
this.accountService.activeAccount$.pipe(
getUserId,
switchMap((userId) =>
this.policyService.masterPasswordPolicyOptions$(userId, policies),
),
),
);
}
return {
policies,

View File

@@ -0,0 +1,38 @@
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 { OrganizationInviteService } from "@bitwarden/common/auth/services/organization-invite/organization-invite.service";
import { GlobalState, GlobalStateProvider } from "@bitwarden/common/platform/state";
export class WebOrganizationInviteService implements 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

@@ -15,6 +15,7 @@ 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 { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request";
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 { KeysRequest } from "@bitwarden/common/models/request/keys.request";
@@ -25,7 +26,6 @@ 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, KdfConfigService, KeyService } from "@bitwarden/key-management";
import { AcceptOrganizationInviteService } from "@bitwarden/web-vault/app/auth/organization-invite/accept-organization.service";
import { RouterService } from "@bitwarden/web-vault/app/core";
import { WebSetInitialPasswordService } from "./web-set-initial-password.service";
@@ -43,7 +43,7 @@ describe("WebSetInitialPasswordService", () => {
let organizationApiService: MockProxy<OrganizationApiServiceAbstraction>;
let organizationUserApiService: MockProxy<OrganizationUserApiService>;
let userDecryptionOptionsService: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>;
let acceptOrganizationInviteService: MockProxy<AcceptOrganizationInviteService>;
let organizationInviteService: MockProxy<OrganizationInviteService>;
let routerService: MockProxy<RouterService>;
beforeEach(() => {
@@ -57,7 +57,7 @@ describe("WebSetInitialPasswordService", () => {
organizationApiService = mock<OrganizationApiServiceAbstraction>();
organizationUserApiService = mock<OrganizationUserApiService>();
userDecryptionOptionsService = mock<InternalUserDecryptionOptionsServiceAbstraction>();
acceptOrganizationInviteService = mock<AcceptOrganizationInviteService>();
organizationInviteService = mock<OrganizationInviteService>();
routerService = mock<RouterService>();
sut = new WebSetInitialPasswordService(
@@ -71,7 +71,7 @@ describe("WebSetInitialPasswordService", () => {
organizationApiService,
organizationUserApiService,
userDecryptionOptionsService,
acceptOrganizationInviteService,
organizationInviteService,
routerService,
);
});
@@ -169,9 +169,7 @@ describe("WebSetInitialPasswordService", () => {
// Assert
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
expect(acceptOrganizationInviteService.clearOrganizationInvitation).toHaveBeenCalledTimes(
1,
);
expect(organizationInviteService.clearOrganizationInvitation).toHaveBeenCalledTimes(1);
});
});
@@ -201,7 +199,7 @@ describe("WebSetInitialPasswordService", () => {
// Assert
await expect(promise).rejects.toThrow();
expect(masterPasswordApiService.setPassword).not.toHaveBeenCalled();
expect(acceptOrganizationInviteService.clearOrganizationInvitation).not.toHaveBeenCalled();
expect(organizationInviteService.clearOrganizationInvitation).not.toHaveBeenCalled();
});
});
});

View File

@@ -9,12 +9,12 @@ import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth
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 { 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 { UserId } from "@bitwarden/common/types/guid";
import { KdfConfigService, KeyService } from "@bitwarden/key-management";
import { AcceptOrganizationInviteService } from "@bitwarden/web-vault/app/auth/organization-invite/accept-organization.service";
import { RouterService } from "@bitwarden/web-vault/app/core";
export class WebSetInitialPasswordService
@@ -32,7 +32,7 @@ export class WebSetInitialPasswordService
protected organizationApiService: OrganizationApiServiceAbstraction,
protected organizationUserApiService: OrganizationUserApiService,
protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
private acceptOrganizationInviteService: AcceptOrganizationInviteService,
private organizationInviteService: OrganizationInviteService,
private routerService: RouterService,
) {
super(
@@ -78,6 +78,6 @@ export class WebSetInitialPasswordService
* as clear the org invite itself that was originally set in state by the AcceptOrganizationComponent.
*/
await this.routerService.getAndClearLoginRedirectUrl();
await this.acceptOrganizationInviteService.clearOrganizationInvitation();
await this.organizationInviteService.clearOrganizationInvitation();
}
}

View File

@@ -9,20 +9,16 @@ 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 { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.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", () => {
@@ -30,32 +26,28 @@ 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>;
let configService: MockProxy<ConfigService>;
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);
configService = mock<ConfigService>();
service = new WebRegistrationFinishService(
keyService,
accountApiService,
acceptOrgInviteService,
organizationInviteService,
policyApiService,
logService,
policyService,
accountService,
configService,
);
});
@@ -76,21 +68,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();
});
});
@@ -106,22 +98,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,
@@ -131,13 +123,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,
@@ -151,14 +143,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,
@@ -225,7 +217,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);
@@ -261,7 +253,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);
@@ -297,7 +289,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,
@@ -338,7 +330,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,
@@ -381,7 +373,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,16 +12,14 @@ 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 { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.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
@@ -29,18 +27,17 @@ 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,
private configService: ConfigService,
) {
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;
}
@@ -50,7 +47,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;
@@ -115,7 +112,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

@@ -5,16 +5,16 @@ import {
SetPasswordCredentials,
SetPasswordJitService,
} from "@bitwarden/auth/angular";
import { OrganizationInviteService } from "@bitwarden/common/auth/services/organization-invite/organization-invite.service";
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);
organizationInviteService = inject(OrganizationInviteService);
override async setPassword(credentials: SetPasswordCredentials) {
await super.setPassword(credentials);
@@ -22,6 +22,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

@@ -1,6 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { FakeGlobalStateProvider } from "@bitwarden/common/../spec/fake-state-provider";
import { MockProxy, mock } from "jest-mock-extended";
import { BehaviorSubject } from "rxjs";
@@ -15,22 +14,18 @@ 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 { 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 +38,8 @@ 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 +52,8 @@ 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 +67,7 @@ describe("AcceptOrganizationInviteService", () => {
organizationApiService,
organizationUserApiService,
i18nService,
globalStateProvider,
dialogService,
organizationInviteService,
accountService,
);
});
@@ -103,8 +93,10 @@ describe("AcceptOrganizationInviteService", () => {
expect(result).toBe(true);
expect(organizationUserApiService.postOrganizationUserAcceptInit).toHaveBeenCalled();
expect(apiService.refreshIdentityToken).toHaveBeenCalled();
expect(globalState.nextMock).toHaveBeenCalledWith(null);
expect(organizationUserApiService.postOrganizationUserAccept).not.toHaveBeenCalled();
expect(organizationInviteService.getOrganizationInvite).not.toHaveBeenCalled();
expect(organizationInviteService.setOrganizationInvitation).not.toHaveBeenCalled();
expect(organizationInviteService.clearOrganizationInvitation).toHaveBeenCalled();
expect(authService.logOut).not.toHaveBeenCalled();
});
@@ -121,13 +113,16 @@ describe("AcceptOrganizationInviteService", () => {
expect(result).toBe(false);
expect(authService.logOut).toHaveBeenCalled();
expect(globalState.nextMock).toHaveBeenCalledWith(invite);
expect(organizationInviteService.setOrganizationInvitation).toHaveBeenCalledWith(invite);
expect(organizationInviteService.clearOrganizationInvitation).toHaveBeenCalled();
});
it("clears the stored invite when a master password policy check is required but the stored invite doesn't match the provided one", async () => {
const storedInvite = createOrgInvite({ email: "wrongemail@example.com" });
const providedInvite = createOrgInvite();
await globalState.update(() => storedInvite);
organizationInviteService.getOrganizationInvite.mockReturnValueOnce(
Promise.resolve(storedInvite),
);
policyApiService.getPoliciesByToken.mockResolvedValue([
{
type: PolicyType.MasterPassword,
@@ -139,7 +134,11 @@ describe("AcceptOrganizationInviteService", () => {
expect(result).toBe(false);
expect(authService.logOut).toHaveBeenCalled();
expect(globalState.nextMock).toHaveBeenCalledWith(providedInvite);
expect(organizationInviteService.setOrganizationInvitation).toHaveBeenCalledWith(
providedInvite,
);
expect(organizationInviteService.getOrganizationInvite).toHaveBeenCalledWith();
expect(organizationInviteService.clearOrganizationInvitation).toHaveBeenCalled();
});
it("accepts the invitation request when the organization doesn't have a master password policy", async () => {
@@ -151,8 +150,10 @@ describe("AcceptOrganizationInviteService", () => {
expect(result).toBe(true);
expect(organizationUserApiService.postOrganizationUserAccept).toHaveBeenCalled();
expect(apiService.refreshIdentityToken).toHaveBeenCalled();
expect(globalState.nextMock).toHaveBeenCalledWith(null);
expect(organizationUserApiService.postOrganizationUserAcceptInit).not.toHaveBeenCalled();
expect(organizationInviteService.setOrganizationInvitation).not.toHaveBeenCalled();
expect(organizationInviteService.getOrganizationInvite).not.toHaveBeenCalled();
expect(organizationInviteService.clearOrganizationInvitation).toHaveBeenCalled();
expect(authService.logOut).not.toHaveBeenCalled();
});
@@ -165,7 +166,7 @@ describe("AcceptOrganizationInviteService", () => {
} as Policy,
]);
// an existing invite means the user has already passed the master password policy
await globalState.update(() => invite);
organizationInviteService.getOrganizationInvite.mockReturnValueOnce(Promise.resolve(invite));
policyService.getResetPasswordPolicyOptions.mockReturnValue([
{
@@ -179,6 +180,8 @@ describe("AcceptOrganizationInviteService", () => {
expect(result).toBe(true);
expect(organizationUserApiService.postOrganizationUserAccept).toHaveBeenCalled();
expect(organizationUserApiService.postOrganizationUserAcceptInit).not.toHaveBeenCalled();
expect(organizationInviteService.getOrganizationInvite).toHaveBeenCalledWith();
expect(organizationInviteService.clearOrganizationInvitation).toHaveBeenCalled();
expect(authService.logOut).not.toHaveBeenCalled();
});
@@ -202,7 +205,7 @@ describe("AcceptOrganizationInviteService", () => {
encryptedString: "encryptedString",
} as EncString);
await globalState.update(() => invite);
organizationInviteService.getOrganizationInvite.mockReturnValueOnce(Promise.resolve(invite));
policyService.getResetPasswordPolicyOptions.mockReturnValue([
{
@@ -220,6 +223,9 @@ describe("AcceptOrganizationInviteService", () => {
);
expect(organizationUserApiService.postOrganizationUserAccept).toHaveBeenCalled();
expect(organizationUserApiService.postOrganizationUserAcceptInit).not.toHaveBeenCalled();
expect(organizationInviteService.getOrganizationInvite).toHaveBeenCalledTimes(1);
expect(organizationInviteService.getOrganizationInvite).toHaveBeenCalledWith();
expect(organizationInviteService.clearOrganizationInvitation).toHaveBeenCalled();
expect(authService.logOut).not.toHaveBeenCalled();
});
});

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,
);
}
@@ -244,8 +244,7 @@ export class ChangePasswordComponent
await this.masterPasswordApiService.postPassword(request);
this.toastService.showToast({
variant: "success",
title: this.i18nService.t("masterPasswordChanged"),
message: this.i18nService.t("masterPasswordChangedDesc"),
message: this.i18nService.t("masterPasswordChanged"),
});
this.messagingService.send("logout");
} catch {

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,7 +1,6 @@
<h1 class="tw-mt-6 tw-mb-2 tw-pb-2.5">{{ "changeMasterPassword" | i18n }}</h1>
<div class="tw-max-w-lg tw-mb-12">
<bit-callout type="warning">{{ "loggedOutWarning" | i18n }}</bit-callout>
<auth-change-password [inputPasswordFlow]="inputPasswordFlow"></auth-change-password>
</div>

View File

@@ -2,7 +2,8 @@ import { Component, OnInit } from "@angular/core";
import { Router } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { ChangePasswordComponent, InputPasswordFlow } from "@bitwarden/auth/angular";
import { ChangePasswordComponent } from "@bitwarden/angular/auth/password-management/change-password";
import { InputPasswordFlow } from "@bitwarden/auth/angular";
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
import { CalloutModule } from "@bitwarden/components";
import { I18nPipe } from "@bitwarden/ui-common";
@@ -16,6 +17,7 @@ import { WebauthnLoginSettingsModule } from "../../webauthn-login-settings";
})
export class PasswordSettingsComponent implements OnInit {
inputPasswordFlow = InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation;
changePasswordFeatureFlag = false;
constructor(
private router: Router,

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,
@@ -115,7 +115,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,
@@ -174,7 +174,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

@@ -10,6 +10,7 @@ import {
OrganizationUserApiService,
CollectionService,
} from "@bitwarden/admin-console/common";
import { ChangePasswordService } from "@bitwarden/angular/auth/password-management/change-password";
import { SetInitialPasswordService } from "@bitwarden/angular/auth/password-management/set-initial-password/set-initial-password.service.abstraction";
import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider";
import {
@@ -34,7 +35,6 @@ import {
SsoComponentService,
LoginDecryptionOptionsService,
TwoFactorAuthDuoComponentService,
ChangePasswordService,
} from "@bitwarden/auth/angular";
import {
InternalUserDecryptionOptionsServiceAbstraction,
@@ -52,6 +52,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";
@@ -108,6 +109,7 @@ import {
} from "@bitwarden/key-management";
import { LockComponentService } from "@bitwarden/key-management-ui";
import { DefaultSshImportPromptService, SshImportPromptService } from "@bitwarden/vault";
import { WebOrganizationInviteService } from "@bitwarden/web-vault/app/auth/core/services/organization-invite/web-organization-invite.service";
import { flagEnabled } from "../../utils/flags";
import { PolicyListService } from "../admin-console/core/policy-list.service";
@@ -122,7 +124,6 @@ import {
WebSetInitialPasswordService,
} 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";
@@ -246,17 +247,21 @@ const safeProviders: SafeProvider[] = [
provide: CLIENT_TYPE,
useValue: ClientType.Web,
}),
safeProvider({
provide: OrganizationInviteService,
useClass: WebOrganizationInviteService,
deps: [GlobalStateProvider],
}),
safeProvider({
provide: RegistrationFinishServiceAbstraction,
useClass: WebRegistrationFinishService,
deps: [
KeyServiceAbstraction,
AccountApiServiceAbstraction,
AcceptOrganizationInviteService,
OrganizationInviteService,
PolicyApiServiceAbstraction,
LogService,
PolicyService,
AccountService,
ConfigService,
],
}),
@@ -275,12 +280,11 @@ const safeProviders: SafeProvider[] = [
provide: SetPasswordJitService,
useClass: WebSetPasswordJitService,
deps: [
ApiService,
MasterPasswordApiService,
KeyServiceAbstraction,
EncryptService,
I18nServiceAbstraction,
KdfConfigService,
KeyServiceAbstraction,
MasterPasswordApiService,
InternalMasterPasswordServiceAbstraction,
OrganizationApiServiceAbstraction,
OrganizationUserApiService,
@@ -301,7 +305,7 @@ const safeProviders: SafeProvider[] = [
OrganizationApiServiceAbstraction,
OrganizationUserApiService,
InternalUserDecryptionOptionsServiceAbstraction,
AcceptOrganizationInviteService,
OrganizationInviteService,
RouterService,
],
}),
@@ -314,7 +318,7 @@ const safeProviders: SafeProvider[] = [
provide: LoginComponentService,
useClass: WebLoginComponentService,
deps: [
AcceptOrganizationInviteService,
OrganizationInviteService,
LogService,
PolicyApiServiceAbstraction,
InternalPolicyService,
@@ -326,6 +330,7 @@ const safeProviders: SafeProvider[] = [
SsoLoginServiceAbstraction,
Router,
AccountService,
ConfigService,
],
}),
safeProvider({
@@ -378,7 +383,7 @@ const safeProviders: SafeProvider[] = [
safeProvider({
provide: LoginDecryptionOptionsService,
useClass: WebLoginDecryptionOptionsService,
deps: [MessagingService, RouterService, AcceptOrganizationInviteService],
deps: [MessagingService, RouterService, OrganizationInviteService],
}),
safeProvider({
provide: IpcService,
@@ -398,6 +403,7 @@ const safeProviders: SafeProvider[] = [
MasterPasswordApiService,
InternalMasterPasswordServiceAbstraction,
UserKeyRotationService,
RouterService,
],
}),
];

View File

@@ -10,6 +10,7 @@ import {
unauthGuardFn,
activeAuthGuard,
} from "@bitwarden/angular/auth/guards";
import { ChangePasswordComponent } from "@bitwarden/angular/auth/password-management/change-password";
import { SetInitialPasswordComponent } from "@bitwarden/angular/auth/password-management/set-initial-password/set-initial-password.component";
import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard";
import {
@@ -144,13 +145,29 @@ const routes: Routes = [
{
path: "update-temp-password",
component: UpdateTempPasswordComponent,
canActivate: [authGuard],
canActivate: [
canAccessFeature(
FeatureFlag.PM16117_ChangeExistingPasswordRefactor,
false,
"change-password",
false,
),
authGuard,
],
data: { titleId: "updateTempPassword" } satisfies RouteDataProperties,
},
{
path: "update-password",
component: UpdatePasswordComponent,
canActivate: [authGuard],
canActivate: [
canAccessFeature(
FeatureFlag.PM16117_ChangeExistingPasswordRefactor,
false,
"change-password",
false,
),
authGuard,
],
data: { titleId: "updatePassword" } satisfies RouteDataProperties,
},
],
@@ -580,6 +597,14 @@ const routes: Routes = [
},
],
},
{
path: "change-password",
component: ChangePasswordComponent,
canActivate: [
canAccessFeature(FeatureFlag.PM16117_ChangeExistingPasswordRefactor),
authGuard,
],
},
{
path: "setup-extension",
data: {