1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-16 08:13:42 +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,20 +0,0 @@
@if (initializing) {
<i
class="bwi bwi-spinner bwi-spin bwi-2x tw-text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
} @else {
<auth-input-password
[flow]="inputPasswordFlow"
[email]="email"
[userId]="userId"
[loading]="submitting"
[masterPasswordPolicyOptions]="masterPasswordPolicyOptions"
[inlineButtons]="true"
[primaryButtonText]="{ key: 'changeMasterPassword' }"
(onPasswordFormSubmit)="handlePasswordFormSubmit($event)"
>
</auth-input-password>
}

View File

@@ -1,114 +0,0 @@
import { Component, Input, OnInit } from "@angular/core";
import { firstValueFrom } from "rxjs";
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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { SyncService } from "@bitwarden/common/platform/sync";
import { UserId } from "@bitwarden/common/types/guid";
// 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 { ToastService } from "@bitwarden/components";
import { I18nPipe } from "@bitwarden/ui-common";
import {
InputPasswordComponent,
InputPasswordFlow,
} from "../input-password/input-password.component";
import { PasswordInputResult } from "../input-password/password-input-result";
import { ChangePasswordService } from "./change-password.service.abstraction";
@Component({
selector: "auth-change-password",
templateUrl: "change-password.component.html",
imports: [InputPasswordComponent, I18nPipe],
})
export class ChangePasswordComponent implements OnInit {
@Input() inputPasswordFlow: InputPasswordFlow = InputPasswordFlow.ChangePassword;
activeAccount: Account | null = null;
email?: string;
userId?: UserId;
masterPasswordPolicyOptions?: MasterPasswordPolicyOptions;
initializing = true;
submitting = false;
constructor(
private accountService: AccountService,
private changePasswordService: ChangePasswordService,
private i18nService: I18nService,
private messagingService: MessagingService,
private policyService: PolicyService,
private toastService: ToastService,
private syncService: SyncService,
) {}
async ngOnInit() {
this.activeAccount = await firstValueFrom(this.accountService.activeAccount$);
this.userId = this.activeAccount?.id;
this.email = this.activeAccount?.email;
if (!this.userId) {
throw new Error("userId not found");
}
this.masterPasswordPolicyOptions = await firstValueFrom(
this.policyService.masterPasswordPolicyOptions$(this.userId),
);
this.initializing = false;
}
async handlePasswordFormSubmit(passwordInputResult: PasswordInputResult) {
this.submitting = true;
try {
if (passwordInputResult.rotateUserKey) {
if (this.activeAccount == null) {
throw new Error("activeAccount not found");
}
if (
passwordInputResult.currentPassword == null ||
passwordInputResult.newPasswordHint == null
) {
throw new Error("currentPassword or newPasswordHint not found");
}
await this.syncService.fullSync(true);
await this.changePasswordService.rotateUserKeyMasterPasswordAndEncryptedData(
passwordInputResult.currentPassword,
passwordInputResult.newPassword,
this.activeAccount,
passwordInputResult.newPasswordHint,
);
} else {
if (!this.userId) {
throw new Error("userId not found");
}
await this.changePasswordService.changePassword(passwordInputResult, this.userId);
this.toastService.showToast({
variant: "success",
title: this.i18nService.t("masterPasswordChanged"),
message: this.i18nService.t("masterPasswordChangedDesc"),
});
this.messagingService.send("logout");
}
} catch {
this.toastService.showToast({
variant: "error",
title: "",
message: this.i18nService.t("errorOccurred"),
});
} finally {
this.submitting = false;
}
}
}

View File

@@ -1,36 +0,0 @@
import { PasswordInputResult } from "@bitwarden/auth/angular";
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
import { UserId } from "@bitwarden/common/types/guid";
export abstract class ChangePasswordService {
/**
* Creates a new user key and re-encrypts all required data with it.
* - does so by calling the underlying method on the `UserKeyRotationService`
* - implemented in Web only
*
* @param currentPassword the current password
* @param newPassword the new password
* @param user the user account
* @param newPasswordHint the new password hint
* @throws if called from a non-Web client
*/
abstract rotateUserKeyMasterPasswordAndEncryptedData(
currentPassword: string,
newPassword: string,
user: Account,
newPasswordHint: string,
): Promise<void>;
/**
* Changes the user's password and re-encrypts the user key with the `newMasterKey`.
* - Specifically, this method uses credentials from the `passwordInputResult` to:
* 1. Decrypt the user key with the `currentMasterKey`
* 2. Re-encrypt that user key with the `newMasterKey`, resulting in a `newMasterKeyEncryptedUserKey`
* 3. Build a `PasswordRequest` object that gets POSTed to `"/accounts/password"`
*
* @param passwordInputResult credentials object received from the `InputPasswordComponent`
* @param userId the `userId`
* @throws if the `userId`, `currentMasterKey`, or `currentServerMasterKeyHash` is not found
*/
abstract changePassword(passwordInputResult: PasswordInputResult, userId: UserId): Promise<void>;
}

View File

@@ -1,177 +0,0 @@
import { mock, MockProxy } from "jest-mock-extended";
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 { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { UserId } from "@bitwarden/common/types/guid";
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
import { KeyService, PBKDF2KdfConfig } from "@bitwarden/key-management";
import { PasswordInputResult } from "../input-password/password-input-result";
import { ChangePasswordService } from "./change-password.service.abstraction";
import { DefaultChangePasswordService } from "./default-change-password.service";
describe("DefaultChangePasswordService", () => {
let keyService: MockProxy<KeyService>;
let masterPasswordApiService: MockProxy<MasterPasswordApiService>;
let masterPasswordService: MockProxy<InternalMasterPasswordServiceAbstraction>;
let sut: ChangePasswordService;
const userId = "userId" as UserId;
const user: Account = {
id: userId,
email: "email",
emailVerified: false,
name: "name",
};
const passwordInputResult: PasswordInputResult = {
currentMasterKey: new SymmetricCryptoKey(new Uint8Array(32)) as MasterKey,
currentServerMasterKeyHash: "currentServerMasterKeyHash",
newPassword: "newPassword",
newPasswordHint: "newPasswordHint",
newMasterKey: new SymmetricCryptoKey(new Uint8Array(32)) as MasterKey,
newServerMasterKeyHash: "newServerMasterKeyHash",
newLocalMasterKeyHash: "newLocalMasterKeyHash",
kdfConfig: new PBKDF2KdfConfig(),
};
const decryptedUserKey = new SymmetricCryptoKey(new Uint8Array(64)) as UserKey;
const newMasterKeyEncryptedUserKey: [UserKey, EncString] = [
decryptedUserKey,
{ encryptedString: "newMasterKeyEncryptedUserKey" } as EncString,
];
beforeEach(() => {
keyService = mock<KeyService>();
masterPasswordApiService = mock<MasterPasswordApiService>();
masterPasswordService = mock<InternalMasterPasswordServiceAbstraction>();
sut = new DefaultChangePasswordService(
keyService,
masterPasswordApiService,
masterPasswordService,
);
masterPasswordService.decryptUserKeyWithMasterKey.mockResolvedValue(decryptedUserKey);
keyService.encryptUserKeyWithMasterKey.mockResolvedValue(newMasterKeyEncryptedUserKey);
});
describe("changePassword()", () => {
it("should call the postPassword() API method with a the correct PasswordRequest credentials", async () => {
// Act
await sut.changePassword(passwordInputResult, userId);
// Assert
expect(masterPasswordApiService.postPassword).toHaveBeenCalledWith(
expect.objectContaining({
masterPasswordHash: passwordInputResult.currentServerMasterKeyHash,
masterPasswordHint: passwordInputResult.newPasswordHint,
newMasterPasswordHash: passwordInputResult.newServerMasterKeyHash,
key: newMasterKeyEncryptedUserKey[1].encryptedString,
}),
);
});
it("should call decryptUserKeyWithMasterKey and encryptUserKeyWithMasterKey", async () => {
// Act
await sut.changePassword(passwordInputResult, userId);
// Assert
expect(masterPasswordService.decryptUserKeyWithMasterKey).toHaveBeenCalledWith(
passwordInputResult.currentMasterKey,
userId,
);
expect(keyService.encryptUserKeyWithMasterKey).toHaveBeenCalledWith(
passwordInputResult.newMasterKey,
decryptedUserKey,
);
});
it("should throw if a userId was not found", async () => {
// Arrange
const userId: null = null;
// Act
const testFn = sut.changePassword(passwordInputResult, userId);
// Assert
await expect(testFn).rejects.toThrow("userId not found");
});
it("should throw if a currentMasterKey was not found", async () => {
// Arrange
const incorrectPasswordInputResult = { ...passwordInputResult };
incorrectPasswordInputResult.currentMasterKey = null;
// Act
const testFn = sut.changePassword(incorrectPasswordInputResult, userId);
// Assert
await expect(testFn).rejects.toThrow(
"invalid PasswordInputResult credentials, could not change password",
);
});
it("should throw if a currentServerMasterKeyHash was not found", async () => {
// Arrange
const incorrectPasswordInputResult = { ...passwordInputResult };
incorrectPasswordInputResult.currentServerMasterKeyHash = null;
// Act
const testFn = sut.changePassword(incorrectPasswordInputResult, userId);
// Assert
await expect(testFn).rejects.toThrow(
"invalid PasswordInputResult credentials, could not change password",
);
});
it("should throw an error if user key decryption fails", async () => {
// Arrange
masterPasswordService.decryptUserKeyWithMasterKey.mockResolvedValue(null);
// Act
const testFn = sut.changePassword(passwordInputResult, userId);
// Assert
await expect(testFn).rejects.toThrow("Could not decrypt user key");
});
it("should throw an error if postPassword() fails", async () => {
// Arrange
masterPasswordApiService.postPassword.mockRejectedValueOnce(new Error("error"));
// Act
const testFn = sut.changePassword(passwordInputResult, userId);
// Assert
await expect(testFn).rejects.toThrow("Could not change password");
expect(masterPasswordApiService.postPassword).toHaveBeenCalled();
});
});
describe("rotateUserKeyMasterPasswordAndEncryptedData()", () => {
it("should throw an error (the method is only implemented in Web)", async () => {
// Act
const testFn = sut.rotateUserKeyMasterPasswordAndEncryptedData(
"currentPassword",
"newPassword",
user,
"newPasswordHint",
);
// Assert
await expect(testFn).rejects.toThrow(
"rotateUserKeyMasterPasswordAndEncryptedData() is only implemented in Web",
);
});
});
});

View File

@@ -1,65 +0,0 @@
import { PasswordInputResult, ChangePasswordService } from "@bitwarden/auth/angular";
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { UserId } from "@bitwarden/common/types/guid";
import { KeyService } from "@bitwarden/key-management";
export class DefaultChangePasswordService implements ChangePasswordService {
constructor(
protected keyService: KeyService,
protected masterPasswordApiService: MasterPasswordApiService,
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
) {}
async rotateUserKeyMasterPasswordAndEncryptedData(
currentPassword: string,
newPassword: string,
user: Account,
hint: string,
): Promise<void> {
throw new Error("rotateUserKeyMasterPasswordAndEncryptedData() is only implemented in Web");
}
async changePassword(passwordInputResult: PasswordInputResult, userId: UserId) {
if (!userId) {
throw new Error("userId not found");
}
if (
!passwordInputResult.currentMasterKey ||
!passwordInputResult.currentServerMasterKeyHash ||
!passwordInputResult.newMasterKey ||
!passwordInputResult.newServerMasterKeyHash ||
passwordInputResult.newPasswordHint == null
) {
throw new Error("invalid PasswordInputResult credentials, could not change password");
}
const decryptedUserKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(
passwordInputResult.currentMasterKey,
userId,
);
if (decryptedUserKey == null) {
throw new Error("Could not decrypt user key");
}
const newMasterKeyEncryptedUserKey = await this.keyService.encryptUserKeyWithMasterKey(
passwordInputResult.newMasterKey,
decryptedUserKey,
);
const request = new PasswordRequest();
request.masterPasswordHash = passwordInputResult.currentServerMasterKeyHash;
request.newMasterPasswordHash = passwordInputResult.newServerMasterKeyHash;
request.masterPasswordHint = passwordInputResult.newPasswordHint;
request.key = newMasterKeyEncryptedUserKey[1].encryptedString as string;
try {
await this.masterPasswordApiService.postPassword(request);
} catch {
throw new Error("Could not change password");
}
}
}

View File

@@ -1,10 +1,6 @@
/**
* This barrel file should only contain Angular exports
*/
// change password
export * from "./change-password/change-password.component";
export * from "./change-password/change-password.service.abstraction";
export * from "./change-password/default-change-password.service";
// fingerprint dialog
export * from "./fingerprint-dialog/fingerprint-dialog.component";

View File

@@ -32,7 +32,7 @@
</bit-form-field>
<div class="tw-mb-6">
<bit-form-field>
<bit-form-field [disableMargin]="true">
<bit-label>{{ "newMasterPass" | i18n }}</bit-label>
<input
id="input-password-form_new-password"

View File

@@ -129,7 +129,7 @@ export class InputPasswordComponent implements OnInit {
@Input({ transform: (val: string) => val?.trim().toLowerCase() }) email?: string;
@Input() userId?: UserId;
@Input() loading = false;
@Input() masterPasswordPolicyOptions: MasterPasswordPolicyOptions | null = null;
@Input() masterPasswordPolicyOptions?: MasterPasswordPolicyOptions;
@Input() inlineButtons = false;
@Input() primaryButtonText?: Translation;
@@ -169,7 +169,7 @@ export class InputPasswordComponent implements OnInit {
protected get minPasswordLengthMsg() {
if (
this.masterPasswordPolicyOptions != null &&
this.masterPasswordPolicyOptions != undefined &&
this.masterPasswordPolicyOptions.minLength > 0
) {
return this.i18nService.t("characterMinimum", this.masterPasswordPolicyOptions.minLength);
@@ -463,7 +463,7 @@ export class InputPasswordComponent implements OnInit {
/**
* Returns `true` if the current password is correct (it can be used to successfully decrypt
* the masterKeyEncrypedUserKey), `false` otherwise
* the masterKeyEncryptedUserKey), `false` otherwise
*/
private async verifyCurrentPassword(
currentPassword: string,

View File

@@ -18,9 +18,12 @@ 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 { 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";
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";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
@@ -122,6 +125,8 @@ 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();
}
@@ -225,7 +230,29 @@ export class LoginComponent implements OnInit, OnDestroy {
return;
}
const credentials = new PasswordLoginCredentials(email, masterPassword);
let credentials: PasswordLoginCredentials;
if (
await this.configService.getFeatureFlag(FeatureFlag.PM16117_ChangeExistingPasswordRefactor)
) {
// Try to retrieve any org policies from an org invite now so we can send it to the
// login strategies. Since it is optional and we only want to be doing this on the
// web we will only send in content in the right context.
const orgPoliciesFromInvite = this.loginComponentService.getOrgPoliciesFromOrgInvite
? await this.loginComponentService.getOrgPoliciesFromOrgInvite()
: null;
const orgMasterPasswordPolicyOptions = orgPoliciesFromInvite?.enforcedPasswordPolicyOptions;
credentials = new PasswordLoginCredentials(
email,
masterPassword,
undefined,
orgMasterPasswordPolicyOptions,
);
} else {
credentials = new PasswordLoginCredentials(email, masterPassword);
}
try {
const authResult = await this.loginStrategyService.logIn(credentials);
@@ -284,7 +311,7 @@ export class LoginComponent implements OnInit, OnDestroy {
This is now unsupported and requires a downgraded client */
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccured"),
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("legacyEncryptionUnsupported"),
});
return;
@@ -325,7 +352,13 @@ export class LoginComponent implements OnInit, OnDestroy {
orgPolicies.enforcedPasswordPolicyOptions,
);
if (isPasswordChangeRequired) {
await this.router.navigate(["update-password"]);
const changePasswordFeatureFlagOn = await this.configService.getFeatureFlag(
FeatureFlag.PM16117_ChangeExistingPasswordRefactor,
);
await this.router.navigate(
changePasswordFeatureFlagOn ? ["change-password"] : ["update-password"],
);
return;
}
}
@@ -337,9 +370,15 @@ export class LoginComponent implements OnInit, OnDestroy {
await this.router.navigate(["vault"]);
}
}
/**
* Checks if the master password meets the enforced policy requirements
* and if the user is required to change their password.
*
* TODO: This is duplicate checking that we want to only do in the password login strategy.
* Once we no longer need the policies state being set to reference later in change password
* via using the Admin Console's new policy endpoint changes we can remove this. Consult
* PM-23001 for details.
*/
private async isPasswordChangeRequiredByOrgPolicy(
enforcedPasswordPolicyOptions: MasterPasswordPolicyOptions,

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,29 @@ 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"]);
}
} else {
await this.router.navigate(["/vault"]);
}
} catch (e) {
this.logService.error(e);
let errorMessage =

View File

@@ -8,7 +8,6 @@ import {
FakeUserDecryptionOptions as UserDecryptionOptions,
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 { OrganizationKeysResponse } from "@bitwarden/common/admin-console/models/response/organization-keys.response";
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
@@ -33,7 +32,6 @@ import { SetPasswordCredentials } from "./set-password-jit.service.abstraction";
describe("DefaultSetPasswordJitService", () => {
let sut: DefaultSetPasswordJitService;
let apiService: MockProxy<ApiService>;
let masterPasswordApiService: MockProxy<MasterPasswordApiService>;
let keyService: MockProxy<KeyService>;
let encryptService: MockProxy<EncryptService>;
@@ -45,7 +43,6 @@ describe("DefaultSetPasswordJitService", () => {
let userDecryptionOptionsService: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>;
beforeEach(() => {
apiService = mock<ApiService>();
masterPasswordApiService = mock<MasterPasswordApiService>();
keyService = mock<KeyService>();
encryptService = mock<EncryptService>();
@@ -57,12 +54,11 @@ describe("DefaultSetPasswordJitService", () => {
userDecryptionOptionsService = mock<InternalUserDecryptionOptionsServiceAbstraction>();
sut = new DefaultSetPasswordJitService(
apiService,
masterPasswordApiService,
keyService,
encryptService,
i18nService,
kdfConfigService,
keyService,
masterPasswordApiService,
masterPasswordService,
organizationApiService,
organizationUserApiService,

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

@@ -394,7 +394,7 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy {
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccured"),
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("legacyEncryptionUnsupported"),
});
return true;
@@ -494,7 +494,7 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy {
return;
}
const defaultSuccessRoute = await this.determineDefaultSuccessRoute();
const defaultSuccessRoute = await this.determineDefaultSuccessRoute(authResult.userId);
await this.router.navigate([defaultSuccessRoute], {
queryParams: {
@@ -503,12 +503,28 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy {
});
}
private async determineDefaultSuccessRoute(): Promise<string> {
private async determineDefaultSuccessRoute(userId: UserId): Promise<string> {
const activeAccountStatus = await firstValueFrom(this.authService.activeAccountStatus$);
if (activeAccountStatus === AuthenticationStatus.Locked) {
return "lock";
}
// TODO: PM-22663 use the new service to handle routing.
if (
await this.configService.getFeatureFlag(FeatureFlag.PM16117_ChangeExistingPasswordRefactor)
) {
const forceSetPasswordReason = await firstValueFrom(
this.masterPasswordService.forceSetPasswordReason$(userId),
);
if (
forceSetPasswordReason === ForceSetPasswordReason.WeakMasterPassword ||
forceSetPasswordReason === ForceSetPasswordReason.AdminForcePasswordReset
) {
return "change-password";
}
}
return "vault";
}