1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-21 18:53:29 +00:00

chore(feature flags): [PM-19034] Remove feature flags and old components for Set/Change Password

* Removed flag and components.

* More cleanup

* Removed ChangePasswordComponent.

* Removed old EmergencyAccessTakeover

* Removed service initialization.

* Fixed test failures.

* Fixed tests.

* Test changes.

* Updated comments

* Fixed tests.

* Fixed tests.

* Fixed merge conflict.

* Removed style and routing references.

* Better comments.

* Removed ResetPasswordComponent
This commit is contained in:
Todd Martin
2025-07-24 12:46:18 -04:00
committed by GitHub
parent df8e0ed094
commit b3db1b79ce
65 changed files with 247 additions and 4487 deletions

View File

@@ -12,7 +12,6 @@ import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/id
import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response";
import { MasterPasswordPolicyResponse } from "@bitwarden/common/auth/models/response/master-password-policy.response";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { FakeMasterPasswordService } from "@bitwarden/common/key-management/master-password/services/fake-master-password.service";
import {
@@ -221,7 +220,10 @@ describe("PasswordLoginStrategy", () => {
await passwordLoginStrategy.logIn(credentials);
expect(policyService.evaluateMasterPassword).not.toHaveBeenCalled();
expect(masterPasswordService.mock.setForceSetPasswordReason).not.toHaveBeenCalledWith(
ForceSetPasswordReason.WeakMasterPassword,
userId,
);
});
it("does not force the user to update their master password when it meets requirements", async () => {
@@ -230,7 +232,10 @@ describe("PasswordLoginStrategy", () => {
await passwordLoginStrategy.logIn(credentials);
expect(policyService.evaluateMasterPassword).toHaveBeenCalled();
expect(masterPasswordService.mock.setForceSetPasswordReason).not.toHaveBeenCalledWith(
ForceSetPasswordReason.WeakMasterPassword,
userId,
);
});
it("when given master password policies as part of the login credentials from an org invite, it combines them with the token response policies to evaluate the user's password as weak", async () => {
@@ -242,12 +247,6 @@ describe("PasswordLoginStrategy", () => {
policyService.evaluateMasterPassword.mockReturnValue(false);
tokenService.decodeAccessToken.mockResolvedValue({ sub: userId });
jest
.spyOn(configService, "getFeatureFlag")
.mockImplementation((flag: FeatureFlag) =>
Promise.resolve(flag === FeatureFlag.PM16117_ChangeExistingPasswordRefactor),
);
credentials.masterPasswordPoliciesFromOrgInvite = Object.assign(
new MasterPasswordPolicyOptions(),
{
@@ -296,9 +295,16 @@ describe("PasswordLoginStrategy", () => {
it("forces the user to update their master password on successful login when it does not meet master password policy requirements", async () => {
passwordStrengthService.getPasswordStrength.mockReturnValue({ score: 0 } as any);
policyService.evaluateMasterPassword.mockReturnValue(false);
tokenService.decodeAccessToken.mockResolvedValue({ sub: userId });
const combinedMasterPasswordPolicyOptions = Object.assign(new MasterPasswordPolicyOptions(), {
enforceOnLogin: true,
});
policyService.combineMasterPasswordPolicyOptions.mockReturnValue(
combinedMasterPasswordPolicyOptions,
);
policyService.evaluateMasterPassword.mockReturnValue(false);
await passwordLoginStrategy.logIn(credentials);
expect(policyService.evaluateMasterPassword).toHaveBeenCalled();
@@ -330,9 +336,16 @@ describe("PasswordLoginStrategy", () => {
it("forces the user to update their master password on successful 2FA login when it does not meet master password policy requirements", async () => {
passwordStrengthService.getPasswordStrength.mockReturnValue({ score: 0 } as any);
policyService.evaluateMasterPassword.mockReturnValue(false);
tokenService.decodeAccessToken.mockResolvedValue({ sub: userId });
const combinedMasterPasswordPolicyOptions = Object.assign(new MasterPasswordPolicyOptions(), {
enforceOnLogin: true,
});
policyService.combineMasterPasswordPolicyOptions.mockReturnValue(
combinedMasterPasswordPolicyOptions,
);
policyService.evaluateMasterPassword.mockReturnValue(false);
const token2FAResponse = new IdentityTwoFactorResponse({
TwoFactorProviders: ["0"],
TwoFactorProviders2: { 0: null },

View File

@@ -12,7 +12,6 @@ import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/ide
import { IdentityDeviceVerificationResponse } from "@bitwarden/common/auth/models/response/identity-device-verification.response";
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { HashPurpose } from "@bitwarden/common/platform/enums";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
@@ -171,35 +170,22 @@ export class PasswordLoginStrategy extends LoginStrategy {
return;
}
// The identity result can contain master password policies for the user's organizations
let masterPasswordPolicyOptions: MasterPasswordPolicyOptions | undefined;
// The identity result can contain master password policies for the user's organizations.
// Get the master password policy options from both the org invite and the identity response.
const masterPasswordPolicyOptions = this.policyService.combineMasterPasswordPolicyOptions(
credentials.masterPasswordPoliciesFromOrgInvite,
this.getMasterPasswordPolicyOptionsFromResponse(identityResponse),
);
// We deliberately do not check enforceOnLogin as existing users who are logging
// in after getting an org invite should always be forced to set a password that
// meets the org's policy. Org Invite -> Registration also works this way for
// new BW users as well.
if (
await this.configService.getFeatureFlag(FeatureFlag.PM16117_ChangeExistingPasswordRefactor)
!credentials.masterPasswordPoliciesFromOrgInvite &&
!masterPasswordPolicyOptions?.enforceOnLogin
) {
// Get the master password policy options from both the org invite and the identity response.
masterPasswordPolicyOptions = this.policyService.combineMasterPasswordPolicyOptions(
credentials.masterPasswordPoliciesFromOrgInvite,
this.getMasterPasswordPolicyOptionsFromResponse(identityResponse),
);
// We deliberately do not check enforceOnLogin as existing users who are logging
// in after getting an org invite should always be forced to set a password that
// meets the org's policy. Org Invite -> Registration also works this way for
// new BW users as well.
if (
!credentials.masterPasswordPoliciesFromOrgInvite &&
!masterPasswordPolicyOptions?.enforceOnLogin
) {
return;
}
} else {
masterPasswordPolicyOptions =
this.getMasterPasswordPolicyOptionsFromResponse(identityResponse);
if (!masterPasswordPolicyOptions?.enforceOnLogin) {
return;
}
return;
}
// If there is a policy active, evaluate the supplied password before its no longer in memory

View File

@@ -10,7 +10,6 @@ import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
import { IUserDecryptionOptionsServerResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/user-decryption-options.response";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { EncryptedString } from "@bitwarden/common/key-management/crypto/models/enc-string";
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
@@ -83,6 +82,7 @@ describe("SsoLoginStrategy", () => {
const ssoCodeVerifier = "SSO_CODE_VERIFIER";
const ssoRedirectUrl = "SSO_REDIRECT_URL";
const ssoOrgId = "SSO_ORG_ID";
const privateKey = "userKeyEncryptedPrivateKey";
beforeEach(async () => {
accountService = mockAccountServiceWith(userId);
@@ -114,6 +114,9 @@ describe("SsoLoginStrategy", () => {
tokenService.decodeAccessToken.mockResolvedValue({
sub: userId,
});
keyService.userEncryptedPrivateKey$
.calledWith(userId)
.mockReturnValue(of(privateKey as EncryptedString));
const mockVaultTimeoutAction = VaultTimeoutAction.Lock;
const mockVaultTimeoutActionBSub = new BehaviorSubject<VaultTimeoutAction>(
@@ -163,6 +166,7 @@ describe("SsoLoginStrategy", () => {
it("sends SSO information to server", async () => {
apiService.postIdentityToken.mockResolvedValue(identityTokenResponseFactory());
keyService.hasUserKey.mockResolvedValue(true);
await ssoLoginStrategy.logIn(credentials);
@@ -185,6 +189,7 @@ describe("SsoLoginStrategy", () => {
it("does not set keys for new SSO user flow", async () => {
const tokenResponse = identityTokenResponseFactory();
tokenResponse.key = null;
tokenResponse.privateKey = null;
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
await ssoLoginStrategy.logIn(credentials);
@@ -210,42 +215,28 @@ describe("SsoLoginStrategy", () => {
);
});
describe("given the PM16117_SetInitialPasswordRefactor feature flag is ON", () => {
beforeEach(() => {
configService.getFeatureFlag.mockImplementation(async (flag) => {
if (flag === FeatureFlag.PM16117_SetInitialPasswordRefactor) {
return true;
}
return false;
});
});
describe("given the user does not have the `trustedDeviceOption`, does not have a master password, is not using key connector, does not have a user key, but they DO have a `userKeyEncryptedPrivateKey`", () => {
it("should set the forceSetPasswordReason to TdeOffboardingUntrustedDevice", async () => {
// Arrange
const mockUserDecryptionOptions: IUserDecryptionOptionsServerResponse = {
HasMasterPassword: false,
TrustedDeviceOption: null,
KeyConnectorOption: null,
};
const tokenResponse = identityTokenResponseFactory(null, mockUserDecryptionOptions);
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
describe("given the user does not have the `trustedDeviceOption`, does not have a master password, is not using key connector, does not have a user key, but they DO have a `userKeyEncryptedPrivateKey`", () => {
it("should set the forceSetPasswordReason to TdeOffboardingUntrustedDevice", async () => {
// Arrange
const mockUserDecryptionOptions: IUserDecryptionOptionsServerResponse = {
HasMasterPassword: false,
TrustedDeviceOption: null,
KeyConnectorOption: null,
};
const tokenResponse = identityTokenResponseFactory(null, mockUserDecryptionOptions);
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
keyService.hasUserKey.mockResolvedValue(false);
keyService.userEncryptedPrivateKey$.mockReturnValue(
of("userKeyEncryptedPrivateKey" as EncryptedString),
);
keyService.hasUserKey.mockResolvedValue(false);
// Act
await ssoLoginStrategy.logIn(credentials);
// Act
await ssoLoginStrategy.logIn(credentials);
// Assert
expect(masterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledTimes(1);
expect(masterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledWith(
ForceSetPasswordReason.TdeOffboardingUntrustedDevice,
userId,
);
});
// Assert
expect(masterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledTimes(1);
expect(masterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledWith(
ForceSetPasswordReason.TdeOffboardingUntrustedDevice,
userId,
);
});
});

View File

@@ -9,7 +9,6 @@ import { SsoTokenRequest } from "@bitwarden/common/auth/models/request/identity-
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
import { HttpStatusCode } from "@bitwarden/common/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
@@ -344,38 +343,18 @@ export class SsoLoginStrategy extends LoginStrategy {
tokenResponse: IdentityTokenResponse,
userId: UserId,
): Promise<void> {
const isSetInitialPasswordFlagOn = await this.configService.getFeatureFlag(
FeatureFlag.PM16117_SetInitialPasswordRefactor,
);
if (isSetInitialPasswordFlagOn) {
if (tokenResponse.hasMasterKeyEncryptedUserKey()) {
// User has masterKeyEncryptedUserKey, so set the userKeyEncryptedPrivateKey
// Note: new JIT provisioned SSO users will not yet have a user asymmetric key pair
// and so we don't want them falling into the createKeyPairForOldAccount flow
await this.keyService.setPrivateKey(
tokenResponse.privateKey ?? (await this.createKeyPairForOldAccount(userId)),
userId,
);
} else if (tokenResponse.privateKey) {
// User doesn't have masterKeyEncryptedUserKey but they do have a userKeyEncryptedPrivateKey
// This is just existing TDE users or a TDE offboarder on an untrusted device
await this.keyService.setPrivateKey(tokenResponse.privateKey, userId);
}
// else {
// User could be new JIT provisioned SSO user in either a MP encryption org OR a TDE org.
// In either case, the user doesn't yet have a user asymmetric key pair, a user key, or a master key + master key encrypted user key.
// }
} else {
// A user that does not yet have a masterKeyEncryptedUserKey set is a new SSO user
const newSsoUser = tokenResponse.key == null;
if (!newSsoUser) {
await this.keyService.setPrivateKey(
tokenResponse.privateKey ?? (await this.createKeyPairForOldAccount(userId)),
userId,
);
}
if (tokenResponse.hasMasterKeyEncryptedUserKey()) {
// User has masterKeyEncryptedUserKey, so set the userKeyEncryptedPrivateKey
// Note: new JIT provisioned SSO users will not yet have a user asymmetric key pair
// and so we don't want them falling into the createKeyPairForOldAccount flow
await this.keyService.setPrivateKey(
tokenResponse.privateKey ?? (await this.createKeyPairForOldAccount(userId)),
userId,
);
} else if (tokenResponse.privateKey) {
// User doesn't have masterKeyEncryptedUserKey but they do have a userKeyEncryptedPrivateKey
// This is just existing TDE users or a TDE offboarder on an untrusted device
await this.keyService.setPrivateKey(tokenResponse.privateKey, userId);
}
}
@@ -431,30 +410,25 @@ export class SsoLoginStrategy extends LoginStrategy {
// - UserDecryptionOptions.UsesKeyConnector is undefined. -- they aren't using key connector
// - UserKey is not set after successful login -- because automatic decryption is not available
// - userKeyEncryptedPrivateKey is set after successful login -- this is the key differentiator between a TDE org user logging into an untrusted device and MP encryption JIT provisioned user logging in for the first time.
const isSetInitialPasswordFlagOn = await this.configService.getFeatureFlag(
FeatureFlag.PM16117_SetInitialPasswordRefactor,
// Why is that the case? Because we set the userKeyEncryptedPrivateKey when we create the userKey, and this is serving as a proxy to tell us that the userKey has been created already (when enrolling in TDE).
const hasUserKeyEncryptedPrivateKey = await firstValueFrom(
this.keyService.userEncryptedPrivateKey$(userId),
);
const hasUserKey = await this.keyService.hasUserKey(userId);
if (isSetInitialPasswordFlagOn) {
const hasUserKeyEncryptedPrivateKey = await firstValueFrom(
this.keyService.userEncryptedPrivateKey$(userId),
// TODO: PM-23491 we should explore consolidating this logic into a flag on the server. It could be set when an org is switched from TDE to MP encryption for each org user.
if (
!userDecryptionOptions.trustedDeviceOption &&
!userDecryptionOptions.hasMasterPassword &&
!userDecryptionOptions.keyConnectorOption?.keyConnectorUrl &&
hasUserKeyEncryptedPrivateKey &&
!hasUserKey
) {
await this.masterPasswordService.setForceSetPasswordReason(
ForceSetPasswordReason.TdeOffboardingUntrustedDevice,
userId,
);
const hasUserKey = await this.keyService.hasUserKey(userId);
// TODO: PM-23491 we should explore consolidating this logic into a flag on the server. It could be set when an org is switched from TDE to MP encryption for each org user.
if (
!userDecryptionOptions.trustedDeviceOption &&
!userDecryptionOptions.hasMasterPassword &&
!userDecryptionOptions.keyConnectorOption?.keyConnectorUrl &&
hasUserKeyEncryptedPrivateKey &&
!hasUserKey
) {
await this.masterPasswordService.setForceSetPasswordReason(
ForceSetPasswordReason.TdeOffboardingUntrustedDevice,
userId,
);
return true;
}
return true;
}
// Check if user has permission to set password but hasn't yet